Trilha Arquitetura Java · TDC São Paulo 2026

Uma feature,
quatro microsserviços

Entregando funcionalidades em ecossistemas Java de alta escala.

Larissa Souza · Software Developer, Pharma @Mercado Livre

Foto de Larissa Souza, mulher negra com cabelo crespo peso, está vestindo camiseta roxo e um sobretudo rose
UMA APAIXONADA PELA TECNOLOGIA
Quem sou eu

Larissa Souza

  • Software Developer na equipe de Pharma do Mercado Livre
  • Uma garota que ama se envolver com a comunidade Tech
  • Aspirante a fotógrafa e videomaker
  • Voluntária e Palestrante em eventos
  • Cursando Tecnólogo em Análise e Desenvolvimento de Sistemas no Senac
  • Técnica em Informática pelo Instituto Federal
Antes de começar: o planejamento

Quando uma tarefa simples vira um monstro distribuído

A ilusão da simplicidade

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.

O poder de uma spike

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.

O gap

A maioria dos tutoriais ensina a criar serviços isolados.
O mundo real não é assim.

O mundo do tutorial

Um endpoint, um banco, um deploy. Adicionar uma funcionalidade é escrever uma classe nova e subir.

O mundo real

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.

O cenário de hoje

A tela de "Parabéns pela compra" de um marketplace

9:41●●● 100%
✓
Compra realizada!Pedido #48213
🚚

Acompanhe seu pedido

Chega em até 3 dias úteis.

🛍️

Produtos relacionados

3 itens que combinam com sua compra.

📄

Meus Direitos

Este produto tem prazo de troca estendido. Saiba como usar

Acompanhar pedido

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.

O desafio

Por que uma feature "simples" precisa de vários microsserviços?

Cada responsabilidade dessas mora, literalmente, num serviço diferente. Antes de ver como eles se conectam, vamos alinhar 3 conceitos.

// Conceito 1 de 3

O que é programação reativa (em 60 segundos)

🧑‍💼

Modelo tradicional (bloqueante)

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.

⚡

Modelo reativo (não-bloqueante)

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.

// Conceito 2 de 3

Uma rota HTTP é só uma anotação em cima de um método

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.

Estilo JAX-RS

@Path("/feedback")
public class FeedbackResource {
  @GET @Path("/{id}")
  public Response get(@PathParam("id") String id) { ... }
}

Estilo usado nos serviços de hoje

@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.

// Conceito 3 de 3

O cliente é burro (e isso é ótimo)

Server-Driven UI (SDUI)

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.

O "brick"

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": "..." }
}
O mapa

As 5 camadas do ecossistema

Backenddetecta contexto
+ fail-safe reativo
→
Config APIconteúdo dinâmico
deploy próprio
→
Middleendorquestra + monta
o componente
→
Frontend SSRrenderiza o que
chegou pronto
→
Trackingmede clique
sem quebrar nada

Nenhuma camada sabe da regra de negócio da outra. Isso é arquitetura, não acidente. Vamos ver isso como um diagrama de fato.

Diagrama de arquitetura

Como as camadas se conectam

flowchart TB U["Usuário
finaliza compra"] --> FE["Frontend SSR"] FE -->|"GET /congrats"| MW["Middleend
monta brick JSON"] MW -->|"GET /feedback"| BE["Backend
decide contexto"] BE -->|"busca conteúdo"| CFG["Config API
texto / ícone / link"] CFG -.-> BE BE -.-> MW MW -.-> FE FE --> TRK["Tracking
mede clique"]
1. Frontend SSRRecebe a tela de pós-compra.
↓ GET /congrats
2. MiddleendOrquestra as fontes e monta o componente.
↓ GET /feedback
3. BackendDecide o contexto e aplica o fail-safe.
↓ busca conteúdo
4. Config APIEntrega texto, ícone e link administrados.
↑ real_state · brick JSON
5. Renderização + trackingO frontend renderiza; o clique é medido sem travar o fluxo.
Diagrama de sequência

O ciclo de vida de uma requisição

sequenceDiagram participant U as Usuário participant FE as Frontend participant MW as Middleend participant BE as Backend participant CFG as Config U->>FE: finaliza a compra FE->>MW: GET /congrats par Mono.zip em paralelo MW->>BE: GET /feedback BE->>CFG: busca conteúdo CFG-->>BE: conteúdo and outras fontes MW->>MW: frete, pontos... end MW-->>FE: brick ou vazio FE-->>U: tela renderizada
Usuáriofinaliza a compra
FrontendGET /congrats
Middleendinicia Mono.zip em paralelo
BackendGET /feedback e busca contexto
Configdevolve conteúdo ou fallback
Frontendrenderiza o brick (ou nenhum)
Conceito-chave

Quem decide não é quem desenha

Backend — decide e busca dados

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.

Middleend — só renderiza

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.

Design de código

Strategy + Factory + switch exaustivo

A estratégia

public class RightsCardStrategy
    implements CardStrategy {

  public boolean handles(CardType t) {
    return t == CardType.MEUS_DIREITOS;
  }

  public Brick buildBrick(ctx) {
    // ícone, título, deeplink
  }
}

O dispatcher — switch exaustivo

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.

O contrato entre serviços

Mudanças aditivas são as únicas seguras

Antes de ativar

"real_state": {
  "meusDireitos": {}
}

Campo existe, vazio. Middleend ignora. Zero risco.

Depois de ativar

"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.

Antes do vilão

Como agregamos 7 fontes de dados em paralelo

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.

O vilão da palestra

O perigo oculto do Mono.zip()

Uma pergunta simples decide o destino das outras 6 chamadas:o que o handler retorna quando dá erro?

Mono.empty()

Cancela o zip inteiro

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.

Mono.just(vazio)

Segue o fluxo normalmente

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.

// O bug

Como o bug se esconde numa linha de fallback


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.

// A correção

A correção é trocar "vazio" por "objeto vazio"


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.

Ao vivo agora

Demo: derrubando (e consertando) o zip()

01

Rodar com o bug

7 fontes em paralelo, 1 retorna Mono.empty(). Ver a resposta inteira cancelada.

02

Aplicar o fix

Trocar por onErrorResume(-> Mono.just(vazio)). Rodar de novo.

03

Comparar

Mesma chamada, resposta completa. Ver o brick sendo montado (ou não) na tela.

→ Abrir Demo interativa agora!

O que a demo mostrou

Um detalhe de default, um efeito cascata

Conceito rápido

O que é uma feature flag?

🎛️

Um interruptor remoto pro código

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.

🛟

Por que isso importa aqui

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.

Obstáculo bônus

Ferramentas de review não têm contexto de arquitetura

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.

O que ninguém te conta no tutorial

Deploy é parte da feature

01 / 05 · MAPEAR ANTES DE CODAR

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.

02 / 05 · EVOLUIR SEM QUEBRAR

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”.

03 / 05 · CONHEÇA A SEMÂNTICA DA FERRAMENTA

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.

05 / 05 · REVIEW PRECISA DE CONTEXTO

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.

04 / 05 · ENTREGA É OPERÁVEL

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

Fim de faixa

Muito obrigada!

Bora se conectar?! Quem está online também pode me chamar por aqui:

Perfil no LinkedIn

Larissa Souza · TDC São Paulo 2026

Material completo (slides + demo)

Repositório no GitHub

Slides · Demo

01 / 30