Há tarefas que chegam para a gente com aquela aparência inocente de:
"É só adicionar um card na tela."
Foi exatamente assim que começou um dos cases mais interessantes que vivi trabalhando como Software Developer no time de Pharma do Mercado Livre.
No papel, parecia uma alteração simples. Na prática, a funcionalidade atravessava quatro microsserviços, contratos entre equipes, programação reativa, Server-Driven UI, feature flag, deploy coordenado, observabilidade e, no meio do caminho, um comportamento do Mono.zip() que eu não esperava encontrar.
Foi esse caso que levei para o palco do TDC São Paulo 2026, na trilha de Arquitetura Java e, depois da palestra, fiquei com a sensação de que esse case não deveria terminar naquele palco. Foi uma experiência que gostei muito de viver e da qual tenho bastante orgulho, principalmente pela autonomia que tive para entender o problema, tomar decisões, investigar um comportamento que eu ainda não conhecia e acompanhar a funcionalidade de ponta a ponta. E por isso, resolvi compartilhar aqui, de uma forma um pouco mais técnica, os aprendizados que levei dessa experiência.
A ilusão da simplicidade
Antes de escrever qualquer código, fiz uma spike para investigar como a tarefa deveria ser feita. E foi aí que veio a primeira descoberta: aquilo que parecia "simples" envolvia quatro microsserviços diretamente na implementação, além de um quinto dentro daquela cadeia de processos, que não exigia uma alteração direta minha.
A funcionalidade era um card condicional na tela de compra finalizada de um marketplace. Um card com informação regulatória que deveria aparecer somente quando fosse uma venda de uma determinada categoria, com conteúdo administrado pelo nosso time.
Como a tarefa tinha prazo, negociei com a minha líder. Eu podia entregar de forma hardcoded, mas se um dia quiséssemos escalar, seria uma dificuldade. Ou podia fazer da forma "certa", sabendo que não atenderíamos ao prazo de negócio. Optamos por não fazer a feature nascer com débito técnico.
E aqui existe um gap. A maioria dos tutoriais de Java ensina Spring Boot, programação reativa e vários conceitos úteis, mas com serviços isolados. No mundo real, principalmente em empresas de larga escala como marketplaces, bancos e fintechs, uma única funcionalidade passa por vários serviços.
Como o ecossistema estava dividido
Cada camada tinha uma responsabilidade:
- Backend: decide o contexto da compra, aplica a regra de negócio, busca o conteúdo do card e devolve o card preenchido, de forma segura e com fail-safe.
- API de configuração: administra o conteúdo de acordo com o país e o contexto. No Brasil vai ser um retorno, na Argentina outro, conforme o time de produto definiu.
- Middleend: a camada de orquestração. Agrega as fontes, monta o componente e define espaçamento e posicionamento.
- Frontend: recebe a peça pronta e a renderiza.
- Tracking: mede os fluxos sem travar nada, para o time de produto olhar o funil e entender quantas pessoas chegaram naquela tela.
Server-Driven UI: o cliente é "burro", e isso é ótimo
Nessa arquitetura, o backend decide qual componente deve ser apresentado e com quais dados. O frontend já conhece esse componente e fica responsável por renderizá-lo:
{
"type": "info_card",
"icon": "document_outline",
"title": "Meus Direitos",
"body": "Este produto tem prazo de troca estendido.",
"action": {
"label": "Ver detalhes",
"url": "..."
}
}
Na prática, o frontend não precisa saber por que aquele card apareceu. Apenas precisa entender como renderizá-lo.
Programação reativa: o garçom que não espera na cozinha
Para explicar programação reativa, uso uma analogia de restaurante. No modelo tradicional, o garçom anota o pedido, entrega na cozinha e fica esperando o prato ficar pronto. Enquanto isso, não atende mais ninguém. No modelo reativo, ele anota o pedido, entrega na cozinha e continua atendendo as outras mesas. Quando o prato fica pronto, ele é avisado e entrega ao cliente.
No Project Reactor, cada pedido é um Mono, que representa zero ou um resultado assíncrono. E aqui vem o detalhe que importa: zero não quer dizer "resultado vazio". Zero quer dizer que nenhum valor foi emitido. Guarde essa diferença. A partir daqui, é que começa o problema que me ensinou mais sobre essa ferramenta.
O bug: quando o "vazio" interrompe o fluxo
O middleend agregava sete fontes em paralelo, contando com a que eu estava adicionando. Cada uma definia um cenário diferente, de acordo com o tipo de compra. Era um fan-out, e usávamos o Mono.zip() para juntar todos os resultados e montar a tela.
Mono.zip(
recomendacoesMono, freteMono, pontosMono,
upsellMono, avaliacaoMono, compartilharMono,
meusDireitosMono
).map(tuple -> montarTela(tuple));
Nesse cenário, o Mono.zip() precisava receber uma emissão de cada fonte para produzir o resultado agregado. O problema estava no meu tratamento de erro:
.onErrorResume(ex -> Mono.empty());
À primeira vista, parece defensivo: "se der erro, retorno vazio". Mas Mono.empty() não emite valor. Quando uma das sete fontes completa sem emitir, o zip também completa sem emitir e cancela as outras seis, inclusive as que já tinham respondido. O resultado da agregação não era produzido como eu esperava. Abri o bug, testei localmente e no ambiente de teste. Eu nunca tinha mexido com reativo daquela forma e não conhecia bem a minha ferramenta.
A correção foi trocar "vazio" por "objeto vazio":
.onErrorResume(ex -> Mono.just(RightsCard.builder().build()));
Agora o Mono emite um valor que significa "não há nada para mostrar". O zip recebe as sete respostas, a tela continua disponível e o middleend simplesmente não desenha o card. A pergunta que importa é esta:
a ausência de card não pode ser ausência de resposta.
Montei uma demo interativa que roda inteira no navegador, sem backend. Ela mostra três cenários: o bug com Mono.empty(), o fix com o card oculto e o fix com o card aparecendo. A diferença é uma linha de código, mas o impacto é o fluxo inteiro do usuário.
Isso reforça algo importante: um código que parece certo pode falhar se não conhecemos a semântica da ferramenta que estamos usando.
Aprendizado 1: mapear antes de codar
Antes de escrever código, precisei descobrir onde a decisão acontece. Tomei três decisões para evitar acoplamento:
- Defini a fonte da decisão. Entendi que deveria ser o backend, e os demais serviços não deveriam repetir a regra de negócio.
- Trabalhei com um contrato aditivo, para que o campo novo nascesse de forma segura.
- Pensei em como fazer as falhas acontecerem cedo, antes de chegar em produção, em um ambiente com usuários de verdade.
Quando a mesma regra passa a existir em vários lugares, cada serviço passa a ter uma versão diferente da verdade. E aí a manutenção fica perigosa.
Aprendizado 2: evoluir sem quebrar
Em sistemas distribuídos, mudanças aditivas são as mais seguras. Quando adicionamos um novo campo ou comportamento, precisamos manter o comportamento anterior funcionando. Muitas vezes a gente adiciona uma funcionalidade nova e acaba quebrando o que já existia. Aí é obrigado a fazer rollback, e vem aquele desespero: quem fez essa tarefa? Será que revisou?
Por isso o campo novo foi adicionado assim:
"real_state": { "meusDireitos": {} }
O campo existe, mas vazio. O que já funcionava continua funcionando. Depois, quando a funcionalidade é ativada, o campo vem preenchido e o middleend monta o card. Isso exige coordenação entre os times. Em sistemas distribuídos, mudança de contrato não é só código, é integração entre equipes.
Aprendizado 3: conheça a semântica da ferramenta
O Mono.zip() foi o vilão da palestra, mas o maior aprendizado não foi decorar uma regra específica do Reactor. Foi entender que abstrações têm semântica.
Eu poderia olhar para Mono.empty() e pensar "estou apenas retornando vazio". Mas, dentro daquele fluxo, não era apenas um valor vazio. Era a ausência de uma emissão, e isso mudava completamente o comportamento da agregação. Quando estamos trabalhando com ferramentas assíncronas, distribuídas ou reativas, entender o que cada operação realmente significa é tão importante quanto saber escrever a sintaxe.
Aprendizado 4: uma entrega precisa ser operável
Código pronto não é entrega pronta. Precisamos colocá-lo em operação com segurança. Antes mesmo do desenvolvimento, precisei escrever um RFC explicando a necessidade, a regra de negócio e como mediríamos o resultado, porque eu ia mexer em código de uma equipe externa.
Durante a revisão, meu líder técnico comentou no documento algo que fez muita diferença: a tarefa estava planejada, mas como colocaríamos aquele código em segurança? Qual a ordem de deploy das quatro aplicações? Se acontecer um problema, como fazer rollback? Qual a métrica? Quais logs?
Minha resposta foi feature flag, um padrão de resiliência que funciona como um interruptor remoto do código. Posso fazer o deploy em produção com o interruptor desligado, e a alteração fica lá, sem aparecer. Depois habilito de acordo com uma condição: um país, um tipo de smartphone, um ambiente específico. Deploy não é release. Chegar em produção não significa estar visível para todo mundo. E como a flag pode ser desligada sem um novo deploy, ela dá um tipo de rollback sem precisar subir a aplicação de novo.
Isso rendeu uma pergunta na plateia: como coordenar feature flag quando a funcionalidade passa por várias equipes? No meu caso, coloquei a flag no backend. Se o middleend recebe o campo vazio, ele não faz nada, então o rollout fica controlado por um lugar só, e as outras camadas apenas consomem quando há conteúdo.
Também precisei pensar em métricas. Latência, taxa de sucesso, comportamento do card. Se estou subindo a alteração esperando que dez por cento das pessoas acessem a tela e a taxa de erro passa de um limite, sei que preciso desligar a flag. E não quero descobrir o problema só quando o cliente entrar em contato com o suporte. Preciso de alertas, monitores e dashboards que me avisem antes.
Uma feature só está realmente pronta quando pode ser ligada, desligada e revertida. Sem isso, o código existe, mas a entrega ainda é arriscada.
E isso ficou provado na prática. Quando chegou a hora de subir para produção, eu estava de férias, mas como a tarefa estava bem definida, minha líder concluiu a negociação que eu havia iniciado, e o outro time fez o rollout com sucesso, pois as métricas, os monitores e todo o plano já estavam documentados e alinhados. Para mim, esse é o melhor teste de uma entrega operável: ela funciona mesmo quando quem escreveu o código não está presente.
Aprendizado 5: review precisa de contexto
Um bot de review apontou uma falha no tratamento da minha feature flag, sugerindo que uma métrica e um alerta que eu havia colocado eram desnecessários. Reli o comentário com atenção, mas mantive a decisão: aquelas diretrizes de observabilidade eram essenciais para garantir o monitoramento correto num cenário de falha envolvendo os serviços do Fury (IDP do Mercado Livre).
O segredo é entender que o bot olha estritamente para o código. Se estiver bem documentado, ele até entende parte do contexto, mas não enxerga a regra de negócio. Quem entende a complexidade do ecossistema somos nós, em conjunto com o time de produto.
Falam que 2026 é o ano da IA agêntica, e ela aumenta a nossa produtividade em várias coisas.** Eu uso IA para investigar problemas, entender arquiteturas que ainda não conheço, explorar bibliotecas, escrever testes e levantar hipóteses.** Mas uma hipótese gerada por uma ferramenta não é uma validação. Um bot pode apontar um risco. Quem precisa entender se aquele risco realmente existe, considerando regra de negócio, arquitetura e comportamento em execução, sou eu.
Não adianta confiar apenas no teste unitário que a IA gerou. Preciso do meu pensamento crítico para saber o que está correto e o que não está em cada cenário da aplicação. Qualquer ferramenta de IA pode até fazer commit, gerar versão, fazer tudo se você liberar. Mas no fim, se acontecer um problema em produção, vão perguntar quem fez aquele código. Eu sou a engenheira responsável por aquela funcionalidade, então é minha responsabilidade.
Recapitulando
Assim, aquilo que começou como "um card" virou uma aula de engenharia de software. Mais do que implementar uma funcionalidade, acompanhei o problema de ponta a ponta: arquitetura, programação reativa, contratos, observabilidade, rollout e revisão de código. Mais do que um código, a gente tem que resolver um problema e entregar uma solução.
Uma feature nunca é só o código que você escreve. É também tudo o que você precisa entender para colocá-la no mundo com segurança.
E talvez esse tenha sido o maior aprendizado que levei desse case: autonomia não é fazer tudo sozinha. **É conseguir entender o problema, fazer as perguntas certas, tomar decisões, buscar ajuda quando necessário e **assumir a responsabilidade pela solução de ponta a ponta.
Material completo da palestra
- Slides: https://eu-larissasouza.github.io/tdc2026-uma-feature-4-microsservicos
- Demo interativa do bug: https://eu-larissasouza.github.io/tdc2026-uma-feature-4-microsservicos/demo-mono-zip
- Repositório: https://github.com/eu-larissasouza/tdc2026-uma-feature-4-microsservicos
Se você trabalha com Java, microsserviços ou programação reativa, vou adorar trocar uma ideia sobre os bugs que te ensinaram alguma coisa.







Top comments (2)
@larysouza parabéns pela iniciativa de começar a escrever e também pela riqueza de detalhes que a sua publicação traz! Só uma sugestão, inclui também a tag
#braziliandevsjá que fala de software e em português brasileiro 💜Muito obrigada pela dica! Inclui a tag