Entregando funcionalidades em ecossistemas Java de alta escala.
Larissa Souza · Software Developer, Pharma @Mercado Livre
No papel, adicionar um card visual parecia uma alteração pontual. Na prática, tocar em múltiplos repositórios revelou acoplamentos invisíveis e riscos de falha em cascata.
Antes do primeiro código oficial, investigar contratos e comportamentos reativos tornou a mudança planejável — e evitou surpresas em produção.
Tutoriais normalmente apresentam serviços isolados. No mundo real, uma spike de entendimento cria o mapa de fronteiras, dependências e planos de contingência que uma entrega distribuída precisa.
Um endpoint, um banco, um deploy. Adicionar uma funcionalidade é escrever uma classe nova e subir.
Sistemas distribuídos: uma única funcionalidade nova exige alterar múltiplos repositórios ao mesmo tempo — cada um com seu próprio ciclo de deploy.
Hoje vou abrir o capô de um caso real: um componente visual novo na tela de pós-compra de um e-commerce de alta escala — e um comportamento oculto do Project Reactor que quase derrubou o fluxo inteiro.
Chega em até 3 dias úteis.
3 itens que combinam com sua compra.
Este produto tem prazo de troca estendido. Saiba como usar
Todo marketplace tem uma tela dessas depois da compra. O card "Meus Direitos" só aparece quando o item comprado pertence a uma categoria regulamentada (garantia estendida, direito de arrependimento, produtos com laudo, etc.) — o resto do tempo, ele simplesmente não existe na tela.
É o exemplo perfeito de um problema que resolvemos em produção: um card condicional, com conteúdo administrado por nosso time, porém que deve ser incluído em um fluxo de alta escala gerenciado por um time externo.
Cada responsabilidade dessas mora, literalmente, num serviço diferente. Antes de ver como eles se conectam, vamos alinhar 3 conceitos.
Uma thread chama um serviço externo e fica parada esperando a resposta. Se a fila crescer, você precisa de mais threads — que custam memória.
Como um garçom que fica plantado na cozinha esperando um único prato ficar pronto, sem atender mais ninguém.
A thread dispara a chamada e segue livre para atender outra coisa. Quando a resposta chega, um callback é acionado. Poucas threads atendem muito mais requisições.
O garçom anota o pedido, entrega pra cozinha e continua atendendo outras mesas — a cozinha o avisa quando o prato sai.
Em Java, isso chega via
Project Reactor (usado pelo Spring
WebFlux): Mono<T> representa 0 ou 1 valor
assíncrono; Flux<T>, uma sequência de valores. Tudo
o que vamos ver hoje é Mono.
Se você já viu @Path / @GET do
JAX-RS (o padrão Jakarta EE para
expor APIs REST em Java) ou @GetMapping do Spring — é a
mesma ideia central: uma anotação mapeia um método Java para uma rota
HTTP.
@Path("/feedback") public class FeedbackResource { @GET @Path("/{id}") public Response get(@PathParam("id") String id) { ... } }
@RestController public class FeedbackController { @GetMapping("/feedback/{id}") public Mono<Feedback> get(@PathVariable String id) { ... } }
A diferença que importa hoje não é a anotação — é o tipo de retorno:
Mono<Feedback> em vez de Feedback. É
isso que torna o endpoint não-bloqueante.
O backend decide o quê mostrar e como — o app/site só recebe um JSON descrevendo o componente e renderiza. Trocar o texto de um card não pede deploy do frontend. É o mesmo princípio de plataformas como o Ghost Platform do Airbnb.
Cada componente visual é um pacote autocontido: tipo, título, ícone, ação. O time de plataforma define os tipos de brick disponíveis; qualquer serviço pode montar um preenchendo os campos.
{ "type": "info_card", "icon": "document_outline", "title": "Meus Direitos", "body": "Este produto tem prazo de troca estendido.", "action": { "label": "Ver detalhes", "url": "..." } }
Nenhuma camada sabe da regra de negócio da outra. Isso é arquitetura, não acidente. Vamos ver isso como um diagrama de fato.
Recebe o contexto da compra, verifica se o item pertence a uma
categoria regulamentada, busca o texto na Config API e devolve um
campo meusDireitos — populado ou vazio.
Lê o campo. Se estiver populado, monta o brick. Se estiver vazio, ignora. O middleend nunca decide por que o card apareceu — só como ele aparece.
Essa separação de responsabilidade é o que permite os dois times evoluírem sem se travar um no outro.
public class RightsCardStrategy implements CardStrategy { public boolean handles(CardType t) { return t == CardType.MEUS_DIREITOS; } public Brick buildBrick(ctx) { // ícone, título, deeplink } }
return switch (cardType) { case MEUS_DIREITOS -> rightsCardStrategy.buildBrick(ctx); case FRETE_GRATIS -> shippingStrategy.buildBrick(ctx); case RECOMENDADO -> recommendedStrategy.buildBrick(ctx); // esquecer um case aqui NÃO COMPILA };
Segurança em tempo de compilação: adicionar um card novo sem registrar o case quebra o build — não a produção. Essa é a "fonte única de verdade" que decide se um card renderiza.
"real_state": { "meusDireitos": {} }
Campo existe, vazio. Middleend ignora. Zero risco.
"real_state": { "meusDireitos": { "title": "Meus Direitos", "deadline": "30 dias" } }
Middleend lê o campo populado e monta o brick.
O middleend precisa conhecer o campo novo antes do backend ir pra produção — mudança de contrato é coisa de dois times, coordenada.
O middleend não espera uma fonte terminar pra chamar a próxima — ele
dispara todas ao mesmo tempo e junta o resultado com
Mono.zip(): recomendações, frete, pontos de fidelidade,
upsell, avaliação, compartilhamento e o card de "Meus Direitos".
Mono.zip(
recomendacoesMono, freteMono, pontosMono,
upsellMono, avaliacaoMono, compartilharMono,
meusDireitosMono
).map(tuple -> montarTela(tuple));
Mono.zip() só emite quando todos os Monos
emitirem. É rápido, é elegante — e tem uma armadilha que não está em
nenhum tutorial de WebFlux.
Mono.zip()
Uma pergunta simples decide o destino das outras 6 chamadas:o que o handler retorna quando dá erro?
Se qualquer uma das 7 fontes emitir vazio, o
Mono.zip() nunca emite — e cancela
todas as outras seis, mesmo as que já tinham respondido.
Retornar um objeto vazio — não um Mono vazio —
dentro de onErrorResume() mantém as outras 6 fontes
intactas. O card simplesmente não aparece.
Isso não está em nenhum tutorial de WebFlux. E vai derrubar sua tela inteira em produção se você não souber.
public Mono<RightsCard> buscarMeusDireitos(PurchaseContext ctx) { return governanceApiClient.getContent(ctx.categoryId()) .onErrorResume(ex -> Mono.empty()); // ⚠️ cancela o zip inteiro }
Parece defensivo — "se der erro, retorno vazio". Mas dentro de um
Mono.zip(), vazio ≠ ausência de valor. Um Mono
vazio nunca emite, e o zip fica esperando para sempre por um valor que
não vai chegar → cancela tudo.
public Mono<RightsCard> buscarMeusDireitos(PurchaseContext ctx) { return governanceApiClient.getContent(ctx.categoryId()) .onErrorResume(ex -> Mono.just(RightsCard.builder().build())); // ✅ fail-safe real }
Agora o Mono emite um valor — só que esse valor representa
"nada para mostrar". O zip() recebe seus 7 valores, monta
a tela normalmente, e o middleend simplesmente não desenha o card.
7 fontes em paralelo, 1 retorna Mono.empty(). Ver a
resposta inteira cancelada.
Trocar por onErrorResume(-> Mono.just(vazio)).
Rodar de novo.
Mesma chamada, resposta completa. Ver o brick sendo montado (ou não) na tela.
Mono.empty(): a resposta inteira nunca chega — as
outras 6 fontes trabalharam à toa.
Mono.just(objetoVazio): a resposta chega completa,
e o card só não aparece.
Uma configuração externa ao código decide, em tempo real, se um trecho roda ou não em produção. Trocar o valor não pede deploy — é uma chamada de API ou um clique num painel.
O card "Meus Direitos" nasce atrás de uma flag: liga por país ou categoria, aos poucos — e se algo der errado, desligar é instantâneo, sem esperar um novo deploy.
if (featureFlags.isEnabled("ff-meus-direitos", ctx.categoryId())) { // busca conteúdo e monta o card }
Guarde esse conceito — ele volta duas vezes: no obstáculo do bot de review e na hora de falar de deploy.
Um bot de code review apontou: "essa chamada de feature flag não tem try-catch — se lançar exceção, quebra tudo."
Investigando o cliente de feature flag: o try-catch já existia, uma camada abaixo, fail-closed por design. O bot analisou o arquivo isolado, sem ver o cliente inteiro.
Saber quando confiar na ferramenta e quando questionar é parte do trabalho — não terceirizável.
case no switch exaustivo não sobe — o
compilador barra.
Antes de escrever código, descubra onde a decisão acontece.
Mapeie quem recebe o pedido, quem decide a regra e quem só exibe o resultado. Assim, cada mudança vai para o serviço certo.
Em sistemas distribuídos, mudanças aditivas são mais seguras.
Ao criar um novo campo ou comportamento, mantenha o comportamento anterior funcionando. Os serviços podem migrar no próprio ritmo, sem uma entrega “tudo ou nada”.
Código que parece certo pode falhar quando não entendemos o que a ferramenta realmente faz.
Foi o caso de Mono.empty() com zip():
“vazio” não significa “um valor sem conteúdo”; significa que nada foi
emitido. Conhecer essa semântica evita bugs escondidos por APIs
aparentemente simples.
Bots de review apontam o risco.
Somente o comportamento real confirma, ou não, um bug.
Use ferramentas para levantar hipóteses, mas valide no fluxo: o que cada operador emite, o que o consumidor espera e qual caso o teste cobre.
Uma feature só está pronta quando
pode ser ligada,
desligada e revertida.
A ordem de deploy, a feature flag e o rollback fazem parte do desenho. Sem isso, o código existe — mas a entrega ainda é arriscada.
"A tecnologia move o
mundo para frente."
— Steve Jobs
Bora se conectar?! Quem está online também pode me chamar por aqui:
Larissa Souza · TDC São Paulo 2026