7. Mensageria Assíncrona
Objetivo
Tirar o trabalho lento e frágil do caminho da requisição no BoxOffice. O serviço order registra cada pedido e um evento OrderPlaced na mesma transação do banco (transactional outbox); um relay publica o evento no Kafka; o serviço notification o consome de forma idempotente, envia o e-mail e estaciona mensagens envenenadas em um tópico de mensagens mortas (DLT). Por fim, uma sala de espera enfileira a multidão da venda relâmpago para que só entrem tantos fãs quanto o sistema consegue atender.
São 10:00 e o show no estádio abre as vendas. Cada POST /orders aceito pelo serviço order também precisa gerar um e-mail de confirmação. Se order chamar notification por HTTP, o fã espera pelo provedor de e-mail, uma queda do e-mail vira uma queda de pedidos, e os 2 milhões de fãs que chegam no mesmo segundo atingem todos os serviços de uma vez. Este lab troca esse acoplamento por três ideias da aula de mensageria: eventos em um log, entrega at-least-once tratada com idempotência e filas que absorvem um pico.
Ele constrói o capítulo 8 da história da disciplina. As decisões que você implementa são justificadas na aula: síncrono vs assíncrono, o transactional outbox, consumidores idempotentes, retries e mensagens mortas e a sala de espera da venda relâmpago.
Slides
Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba
Antes de começar
| Tempo | cerca de 4 horas (dois encontros) |
| Ferramentas | Docker com Compose v2 (docker compose version), k6 1.x (k6 version), curl, jq — todos do lab 0. Os serviços Java são construídos dentro do Docker: não é preciso JDK nem Maven locais neste lab |
| Recursos do Docker | pelo menos 4 CPUs e 8 GB de memória (o Kafka e mais duas JVMs entram na stack do lab 6) |
| Portas | livres na sua máquina: 8084, 8086, 8089, 8025, 1025, 9094 (além das do lab 6) |
Seu repositório precisa estar no estado em que o lab 6 o deixou. Todo comando deste lab roda na raiz do seu repositório boxoffice.
boxoffice/
├── docker-compose.yaml # db, db-replica, pgbouncer, redis, catalog-1..3, inventory, auth, gateway, edge
├── infra/ # nginx/, postgres/, pgbouncer/
├── load/ # k6 scripts of labs 0-6
├── scripts/
└── services/
├── auth/
├── catalog/
├── gateway/
└── inventory/
Recupere o atraso
Pulou ou quebrou o lab 6? Baixe boxoffice-after-lab-6.zip, descompacte, cd boxoffice e continue daqui.
Todo arquivo deste lab aparece completo nestas páginas, com o caminho no título, e também é publicado em https://insper.github.io/platform/2027.1/hands-on/7/code/<caminho> — por exemplo curl -fsSL -o docker-compose.yaml https://insper.github.io/platform/2027.1/hands-on/7/code/docker-compose.yaml.
Arquitetura
flowchart LR
fan[Fã] -->|POST /orders| gw[gateway]
gw --> order[order]
order -->|INSERT do pedido| db[(PostgreSQL)]
order -->|"POST /notifications (espera)"| notification[notification]
notification -->|SMTP| mail[mailpit]
notification -->|201 depois do e-mail| order
order -->|201| gw flowchart LR
fan[Fã] -->|entra| wr[sala de espera]
wr -->|admitido| gw[gateway]
gw -->|POST /orders| order[order]
order -->|"INSERT pedido + outbox (1 tx)"| db[(PostgreSQL)]
order -->|201 na hora| gw
relay[relay do outbox] -->|busca não publicados| db
relay -->|publica OrderPlaced| kafka[(Kafka: order-events)]
kafka -->|grupo de consumidores notification| notification[notification]
notification -->|"processed_events (dedup)"| ndb[(PostgreSQL)]
notification -->|SMTP| mail[mailpit]
notification -.->|após 3 tentativas falhas| dlt[(order-events-dlt)] A requisição do fã agora termina no commit do banco. Tudo o que vem depois — publicar, enviar o e-mail, tentar de novo — acontece de forma assíncrona e pode ser lento, falhar ou se repetir sem que o fã perceba.
Componentes do BoxOffice envolvidos
| Componente da paleta | Neste lab |
|---|---|
| Kafka | Kafka 4 de um nó em modo KRaft, tópico order-events com 6 partições |
| Message Queue | a sala de espera: uma FIFO de fãs no Redis, esvaziada a uma taxa fixa de admissão |
| App Server | order (produtor, relay do outbox, sala de espera) |
| Notifications | notification (consumidor idempotente, DLT) |
| Mailpit, um servidor SMTP local com caixa de entrada web |
O que você vai medir
| Pergunta | Métrica | Ferramenta |
|---|---|---|
Quanto mais rápido fica o POST /orders sem o e-mail em linha? | p50 / p95 / p99, taxa de erro | k6 |
O que acontece com os pedidos enquanto notification está fora? | taxa de erro, lag do consumidor | k6, kafka-consumer-groups.sh |
| Uma rajada forma backlog? Em quanto tempo ele é drenado? | lag ao longo do tempo, tempo de drenagem, atraso p95 do e-mail | kafka-consumer-groups.sh, SQL |
| A reentrega envia e-mails duplicados? | e-mails por pedido | API do Mailpit |
| A sala de espera mantém a taxa de admissão durante um pico? | admitidos/s, posição na fila, p95 dos pedidos | k6 |
Pré-requisitos
- Labs 1 a 6 concluídos: seu repositório
boxofficerodadb(PostgreSQL),redis,catalog,inventorye ogatewaycomdocker compose. - Cerca de 2 GB extras de memória para o Docker (o Kafka é uma JVM).
- Leia a aula de mensageria, em especial Kafka, semânticas de entrega e o padrão outbox.
Passos
- Kafka no Compose — um broker KRaft, o tópico
order-events, o Kafka UI e o Mailpit; um passeio pela CLI. - Transactional outbox — o serviço
ordergrava o pedido e o evento em uma transação; um relay o publica. - Consumidor idempotente e DLT — o serviço
notificationdeduplica poreventId, tenta de novo com backoff e manda mensagens envenenadas para a DLT. - Sala de espera — uma FIFO no Redis que admite fãs a uma taxa fixa durante a abertura das vendas.
- Meça — latência síncrona vs assíncrona, comportamento na queda, lag e drenagem, duplicatas, taxa de admissão.
Experimente no playground
Antes de escrever código, veja as duas formas que este lab constrói. Experimente: no primeiro simulador, rode o perfil load e observe cada serviço consumidor receber sua própria cópia de cada evento (fan-out); depois aumente o RPS alvo até um consumidor ficar para trás enquanto o produtor continua rápido. No segundo, rode spike e observe o backlog da fila crescer e drenar enquanto os workers seguem em ritmo constante.
O simulador completo está no playground.
Meça
O passo Meça tem os scripts k6 e a tabela de resultados. Preencha-a e guarde-a no seu repositório: ela faz parte da entrega.
Solução de problemas
| Sintoma | Causa provável | Correção |
|---|---|---|
kafka fica starting/unhealthy; kafka-init nunca roda | pouca memória no Docker, ou porta 9094 ocupada | dê 8 GB ao Docker; lsof -i :9094; docker compose logs kafka |
order/notification reiniciam com Connection to node -1 … could not be established | subiram antes do Kafka (só se você removeu o depends_on) | docker compose up -d order notification de novo quando o kafka estiver saudável |
order falha com Unable to obtain connection from database | db ainda não saudável, ou variáveis DB_* erradas | docker compose ps db; as variáveis estão no serviço order do docker-compose.yaml |
POST /orders responde 403 on-sale in progress | a sala de espera daquele show está aberta (passo 4) | use outro showId, ou curl -X POST localhost:8084/orders/waiting-room/show-42/close |
| Nenhum e-mail no Mailpit | notification fora, atrasado, ou o evento foi para a DLT | docker compose logs notification; sh scripts/kafka-lag.sh; consulte notification.dead_letters |
docker compose exec kafka … falha com service "kafka" is not running | o broker foi parado em um experimento | docker compose start kafka |
O build falha com Could not resolve dependencies | sem Internet durante o docker compose build | tente de novo com rede; o Maven baixa uma vez e guarda em cache (--mount=type=cache) |
Apple Silicon: no matching manifest for linux/arm64 | imagem sem variante arm64 | todas as imagens deste lab são multiarquitetura; confira se digitou as tags exatamente |
Boas práticas neste lab
- Escreva o evento na mesma transação que a mudança de negócio — o
OrderService.javachamaOutbox.append(que se recusa a rodar fora de uma transação) e oOutboxRelay.javapublica depois do commit. Por quê: transactional outbox. - Torne o consumidor idempotente no seu próprio banco — o
NotificationService.javainsere oeventIdemprocessed_events(V1__create_notification.sql) na mesma transação que a linha do e-mail. Por quê: consumidores idempotentes. - Limite os retries e guarde as mensagens envenenadas para inspeção —
KafkaConfig.java(3 retries, depoisorder-events-dlt; erros de parse sem retry) eDeadLetterListener.java(a tabeladead_letters). Por quê: retries e filas de mensagens mortas. - Crie os tópicos explicitamente e produza de forma durável —
docker-compose.yaml(kafka-init, criação automática desligada) eservices/order/src/main/resources/application.yaml(acks: all,enable.idempotence: true). Por quê: replicação e durabilidade. - Admita a multidão a uma taxa configurada e medida, numa ordem decidida pelo servidor —
WaitingRoom.java(sequência porINCR,ADMIT_PER_SECOND). Por quê: a sala de espera da venda relâmpago.
Limpeza
Ao terminar (ou antes do lab 8), pare a stack e mantenha os dados, ou remova tudo:
docker compose stop # keep containers and volumes for the next lab
docker compose down -v # or: remove containers, networks and volumes (all data)
O lab 8 começa deste estado; não é preciso guardar nada além do seu repositório.
Estado final
boxoffice/
├── docker-compose.yaml # + kafka, kafka-init, kafka-ui, mailpit, order, notification
├── load/
│ ├── orders-load.js
│ └── waiting-room-spike.js
├── scripts/
│ └── kafka-lag.sh
└── services/
├── notification/ # new: idempotent consumer, DLT, e-mail
└── order/ # new: orders, outbox relay, waiting room
Faça commit e tag: git add -A && git commit -m "lab 7: async messaging" && git tag lab7.
Checklist
-
kafka,kafka-init,kafka-uiemailpitrodam nodocker-compose.yaml;order-eventseorder-events-dlttêm 6 partições cada -
ordergravaorderseoutboxem uma transação; repetir oPOST /orderscom o mesmoholdIddevolve o mesmo pedido e não cria evento novo - o relay do outbox publica com chave =
orderIde marca as linhas como publicadas; parar o Kafka não perde nenhum evento -
notificationenvia um e-mail por pedido mesmo depois de um reset de offsets (duplicatas são ignoradas) - uma mensagem envenenada chega à
order-events-dltdepois das tentativas, e a partição continua andando - a sala de espera admite na taxa configurada; um
POST /orderssem ticket de admissão é recusado durante a abertura das vendas - tabela de resultados do passo Meça preenchida em
results/lab7.md, com uma explicação curta de cada número - autoverificação:
docker compose down -v && docker compose up -d --builda partir de um clone limpo sobe tudo saudável; depois, push para o GitHub com a taglab7
Relacionados
- Teoria: Mensageria e eventos · Filas e pub/sub · Kafka · Padrões: outbox, saga, CQRS · Semânticas de entrega
- Estudo de caso: Venda de ingressos (venda relâmpago do BoxOffice)
- Exercício: Orientado a eventos
- Próximo lab: 8. Observabilidade rastreia este fluxo de ponta a ponta, inclusive o salto pelo Kafka.