Arquitetura Orientada a Eventos
REST síncrono funciona bem quando quem chama precisa de uma resposta imediata. Mas, quando um pedido é feito, o sistema pode precisar avisar 5 serviços downstream — e-mail, estoque, detecção de fraude, entrega, analytics. Chamar todos eles de forma síncrona antes de responder ao usuário gera um abismo de latência e uma explosão de acoplamento. A arquitetura orientada a eventos resolve isso com um contrato fundamentalmente diferente: publique um evento e deixe os interessados reagirem de forma assíncrona.
Objetivos de aprendizagem
Ao final desta aula, você deve ser capaz de:
- Decidir quando uma chamada entre dois serviços deve ser uma requisição síncrona ou um evento assíncrono.
- Explicar como o Kafka organiza os dados em tópicos, partições e offsets, e como os grupos de consumidores oferecem tanto fan-out quanto divisão de carga.
- Escolher uma chave de mensagem que preserve a ordem de que o negócio precisa e configurar a durabilidade do produtor (
acks, replicação). - Explicar as entregas at-most-once, at-least-once e exactly-once, e projetar um consumidor idempotente.
- Aplicar o transactional outbox e o padrão saga para manter os dados consistentes entre serviços sem transações distribuídas.
- Comparar event sourcing e CQRS com um modelo CRUD clássico e tratar mensagens envenenadas (poison messages) com uma dead letter queue.
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
Por que orientado a eventos?
Dois modelos de entrega lado a lado: a cadeia síncrona obriga o produtor a esperar cada serviço downstream antes de responder; o modelo assíncrono retorna imediatamente após publicar.
O produtor precisa esperar que cada serviço downstream termine antes de devolver uma resposta ao cliente. A latência se acumula, e uma única dependência lenta (ou com falha) trava a requisição inteira.
sequenceDiagram
autonumber
participant Client
participant OrderService
participant EmailService
participant InventoryService
participant FraudService
Client->>+OrderService: POST /orders
OrderService->>+EmailService: sendConfirmation()
EmailService-->>-OrderService: ok
OrderService->>+InventoryService: reserveStock()
InventoryService-->>-OrderService: ok
OrderService->>+FraudService: checkFraud()
FraudService-->>-OrderService: ok
OrderService-->>-Client: 201 Created O produtor publica um único evento e retorna imediatamente. Cada serviço downstream consome o evento de forma independente — em paralelo, no seu próprio ritmo, sem que o produtor saiba ou se importe.
sequenceDiagram
participant Client
participant OrderService
participant Kafka
participant EmailService
participant InventoryService
participant FraudService
Client->>+OrderService: POST /orders
OrderService->>Kafka: publica OrderCreated
OrderService-->>-Client: 202 Accepted
Kafka-->>EmailService: OrderCreated (assíncrono)
Kafka-->>InventoryService: OrderCreated (assíncrono)
Kafka-->>FraudService: OrderCreated (assíncrono) | Aspecto | Síncrono (REST) | Assíncrono (evento) |
|---|---|---|
| Latência | Soma de todas as chamadas | Apenas a latência do produtor |
| Acoplamento | O produtor conhece todos os consumidores | O produtor não conhece nenhum |
| Resiliência | Um consumidor lento bloqueia todos | Falhas de consumidores são independentes |
| Vazão (throughput) | Limitada pela dependência mais lenta | Consumidores escalam de forma independente |
| Depuração | Rastreamento simples da requisição | Exige rastreamento distribuído |
| Consistência | Forte (síncrona) | Eventual |
A simulação abaixo ilustra o que acontece com a profundidade da fila quando o consumidor não acompanha o produtor — e o que acontece quando ele consegue alcançá-lo.
Conceitos fundamentais
| Conceito | Definição | Exemplo |
|---|---|---|
| Evento | Um registro imutável de algo que aconteceu | OrderCreated { orderId, customerId, timestamp } |
| Mensagem | Contêiner genérico de dados enviado entre serviços | Qualquer payload em uma fila |
| Comando | Uma instrução para fazer algo (pode ser rejeitada) | PlaceOrder { items, paymentToken } |
| Produtor / Consumidor | O serviço que escreve / lê mensagens | o order-service produz; o notification-service consome |
| Broker | O servidor que armazena as mensagens e as entrega aos consumidores | Um broker Kafka (kafka:9092) |
| Tópico | Um log de eventos nomeado, durável e ordenado | order-events |
| Partição | Um fragmento (shard) do tópico para paralelismo; a ordem só é mantida dentro dele | Partições 0, 1, 2 de order-events |
| Chave | Valor opcional que decide a partição de uma mensagem | orderId |
| Offset | Inteiro sequencial que identifica a posição de uma mensagem | Offset 42 na partição 1 |
| Grupo de consumidores | Um conjunto de consumidores que divide o trabalho de um tópico | notification-group |
Eventos são fatos
Diferentemente de registros de banco de dados, eventos nunca são atualizados nem apagados — o log é append-only (só aceita inserções no final). Essa imutabilidade torna seguro reprocessar (replay) os eventos para reconstruir o estado, auditar o histórico ou popular novas projeções sem tocar nos dados originais.
Arquitetura do Kafka
O Apache Kafka1 é um log distribuído e append-only: produtores acrescentam mensagens ao final, consumidores leem a partir de qualquer posição que escolherem, e as mensagens continuam no disco depois de lidas (até que a política de retenção as apague). Essa é a principal diferença em relação a uma fila de mensagens clássica, como o RabbitMQ, em que a mensagem é removida assim que um consumidor confirma o recebimento.
O Kafka organiza as mensagens em tópicos, cada um dividido em partições ordenadas. Vários grupos de consumidores podem ler o mesmo tópico de forma independente — cada grupo mantém seu próprio ponteiro de offset por partição.
flowchart LR
subgraph broker ["Broker Kafka"]
subgraph topic ["Tópico: order-events"]
P0["Partição 0\n0, 1, 2, 3, →"]
P1["Partição 1\n0, 1, 2, →"]
P2["Partição 2\n0, 1, →"]
end
end
subgraph ng ["notification-group"]
N0[Consumidor A]
N1[Consumidor B]
N2[Consumidor C]
end
subgraph ag ["analytics-group"]
A0[Consumidor X]
end
P0 --> N0
P1 --> N1
P2 --> N2
P0 --> A0
P1 --> A0
P2 --> A0 Cada grupo de consumidores controla seus próprios offsets, então notification-group e analytics-group avançam pelo log de forma independente. Adicionar um novo grupo de consumidores nunca afeta os grupos existentes.
Políticas de retenção controlam por quanto tempo o Kafka mantém as mensagens:
| Política | Configuração | Comportamento |
|---|---|---|
| Por tempo | retention.ms=604800000 | Apaga mensagens com mais de 7 dias |
| Por tamanho | retention.bytes=1073741824 | Apaga os segmentos mais antigos quando uma partição passa de 1 GB (o limite é por partição, não por tópico) |
| Compactação | cleanup.policy=compact | Mantém apenas a última mensagem de cada chave (útil para snapshots de estado) |
retention.ms=604800000 (7 dias) é o padrão do Kafka; retention.bytes tem padrão -1 (sem limite de tamanho).
Chaves, partições e ordem
Quando um produtor envia uma mensagem com chave, o particionador padrão do Kafka calcula o hash da chave para escolher a partição: a mesma chave sempre vai para a mesma partição (enquanto o número de partições não mudar). Mensagens sem chave são espalhadas entre as partições.
Como a ordem só é garantida dentro de uma partição, a chave é a forma de escolher o que precisa permanecer em ordem. No Hands-on 9, o order service envia kafkaTemplate.send("order-events", order.getId().toString(), event) — o orderId é a chave, então OrderCreated, OrderPaid e OrderShipped de um mesmo pedido são sempre lidos em sequência, enquanto pedidos diferentes são processados em paralelo.
flowchart LR
p["order-service<br/>(produtor)"] -->|"key=order-7"| h{"hash(key) % 3"}
h -->|"order-7 → P1"| P1["Partição 1"]
h -.->|"outras chaves"| P0["Partição 0"]
h -.->|"outras chaves"| P2["Partição 2"]
P1 --> c["consumidor B<br/>lê os eventos de order-7 em ordem"] Escolhendo a chave
- Uma chave com poucos valores (por exemplo,
country) cria partições quentes (hot partitions): uma partição recebe a maior parte do tráfego enquanto as outras ficam ociosas. - Adicionar partições depois muda
hash(key) % partitions, então chaves existentes podem migrar para outra partição e perder a ordem por um tempo — dimensione as partições desde o início. - Sem chave nenhuma, não há ordem entre eventos relacionados.
Durabilidade: replicação e acks
Em produção, cada partição é replicada em vários brokers (fator de replicação, tipicamente 3). Uma réplica é a líder e recebe todas as escritas; as outras são seguidoras (followers) que a copiam. As réplicas atualizadas formam o ISR (in-sync replicas). Se o broker da líder cair, uma seguidora em sincronia é eleita líder.
A configuração acks do produtor escolhe quanto disso o produtor espera:
acks | O produtor recebe confirmação quando… | Risco |
|---|---|---|
0 | a mensagem foi enviada (sem confirmação) | Perdida se o broker nunca a recebeu |
1 | a líder a gravou | Perdida se a líder morrer antes de as seguidoras copiarem |
all | todas as réplicas em sincronia a têm (com min.insync.replicas ≥ 2) | Mais lenta, mas sobrevive à perda de um broker |
O Hands-on 9 usa acks: all com enable.idempotence: true; o produtor idempotente anexa um número de sequência a cada mensagem, para que os retries do próprio produtor não criem duplicatas no log. (Desde o Kafka 3.0, ambos já são o padrão do produtor.)
ZooKeeper e KRaft
Clusters Kafka mais antigos guardavam seus metadados em um ensemble ZooKeeper separado. Hoje o Kafka tem seu próprio quórum de metadados baseado em Raft (KRaft; veja Consenso), e o Kafka 4.0 (2025) removeu completamente o suporte ao ZooKeeper. O Hands-on 9 roda o Kafka 4 em modo KRaft: um único nó que é broker e controller ao mesmo tempo.
Padrões de mensageria
Pub/Sub (fan-out)
Um evento publicado é entregue a todos os grupos de consumidores interessados. Cada grupo recebe uma cópia completa e independente da mensagem. Como os grupos controlam seus próprios offsets, um pipeline de analytics lento não afeta o pipeline de notificações.
Casos de uso: notificações por e-mail, logs de auditoria, pipelines de analytics, invalidação de cache.
Consumidores concorrentes (competing consumers)
Vários consumidores dentro do mesmo grupo dividem as partições — cada partição pertence a exatamente um consumidor por vez, então cada mensagem é processada por exatamente um consumidor. Esse padrão permite escalar o processamento horizontalmente sem trabalho duplicado.
Casos de uso: processamento de pedidos, tratamento de pagamentos, jobs de redimensionamento de imagens.
O paralelismo máximo de um grupo é o número de partições: com 3 partições, um quarto consumidor no mesmo grupo fica ocioso. Quando um consumidor entra ou sai, o Kafka faz o rebalanceamento (rebalance) do grupo e redistribui as partições.
Event Sourcing
Em vez de armazenar apenas o estado atual, o sistema armazena o histórico completo de eventos3. O estado atual é obtido reprocessando todos os eventos desde o início (ou a partir de um snapshot).
sequenceDiagram
actor User
User->>+CommandHandler: PlaceOrder
CommandHandler->>+EventStore: append OrderCreated
EventStore-->>-CommandHandler: ack
CommandHandler-->>-User: 202 Accepted
EventStore-->>Projection: evento OrderCreated
Projection->>ReadModel: atualiza a visão do pedido Isso significa que o modelo de escrita é o próprio log de eventos. As projeções (modelos de leitura) podem ser reconstruídas a partir dos eventos a qualquer momento, o que permite depuração com "viagem no tempo" e migração de esquema sem downtime.
CQRS (Command Query Responsibility Segregation)
Separe o lado de escrita (comandos, eventos, event store) do lado de leitura (consultas, projeções, modelo de leitura)4. O lado de escrita é otimizado para consistência e auditabilidade; o lado de leitura, para desempenho de consulta.
flowchart LR
client --> |"POST /orders (Comando)"| cmd["Command Handler"]
cmd --> |"OrderCreated"| bus["Barramento de eventos (Kafka)"]
bus --> |"projeta"| view["Modelo de leitura\n(otimizado para consultas)"]
client --> |"GET /orders (Consulta)"| view
cmd --> |"append"| store["Event Store"] O CQRS costuma ser combinado com Event Sourcing, porque o log de eventos é o modelo de escrita natural. Mas o CQRS também pode ser aplicado sem event sourcing — usando bancos separados de leitura e escrita, mantidos em sincronia via change-data-capture (CDC: uma ferramenta como o Debezium lê o log de transações do banco e publica cada alteração como um evento).
Event sourcing e CQRS não são o padrão
Ambos adicionam complexidade real: os eventos precisam ser versionados para sempre, os modelos de leitura são eventualmente consistentes e reconstruir projeções leva tempo. Use-os nas partes do domínio que precisam de trilha de auditoria ou de formatos de leitura e escrita muito diferentes (por exemplo, histórico de pedidos, livros-razão) — não em toda entidade CRUD da loja.
Garantias de entrega e consumidores idempotentes
Um consumidor lê uma mensagem, a processa (por exemplo, envia um e-mail) e faz o commit do offset para dizer ao Kafka "terminei até aqui". Uma queda pode acontecer entre quaisquer dois desses passos, e quando você faz o commit decide quanto uma queda custa:
| Garantia | Como | Uma queda significa | Uso típico |
|---|---|---|---|
| At-most-once | Commit antes de processar | A mensagem é perdida | Métricas em que uma lacuna é aceitável |
| At-least-once | Commit depois de processar | A mensagem é processada de novo | A escolha padrão — Hands-on 9 (enable_auto_commit=False + commit() após o processamento) |
| Exactly-once | Transações do Kafka (produtor idempotente + isolation.level=read_committed) | Nenhuma das duas — mas só para ler do Kafka → escrever no Kafka | Processamento de streams (Kafka Streams) |
sequenceDiagram
participant K as Kafka
participant N as notification-service
participant M as Servidor de e-mail
K->>N: OrderCreated (offset 42)
N->>M: envia o e-mail de confirmação
Note over N: cai antes do commit
N->>K: (reinício) busca a partir do último offset commitado
K->>N: OrderCreated (offset 42) de novo
N->>N: já enviado para o pedido 7? sim → ignora
N->>K: commit do offset 43 O exactly-once dentro do Kafka não se estende a efeitos colaterais fora dele: o Kafka não consegue "desenviar" um e-mail nem desfazer uma linha no Postgres. Na prática, os sistemas usam entrega at-least-once + consumidores idempotentes: processar a mesma mensagem duas vezes tem o mesmo efeito que processá-la uma vez.
Formas comuns de tornar um consumidor idempotente:
- Tabela de mensagens processadas: guarde o ID do evento (ou
orderId+ tipo do evento) no banco do consumidor, na mesma transação da alteração de negócio; ignore eventos já armazenados. - Idempotência natural: use operações de escrita idempotentes por natureza —
UPSERT, "definir status como PAID" em vez de "somar 10 ao saldo". - Restrições de unicidade: deixe o banco rejeitar a segunda inserção do mesmo
orderId.
Mantendo os serviços consistentes
O problema da escrita dupla (dual write)
No Hands-on 9, o order service salva o pedido no Postgres e depois publica OrderCreated no Kafka. São dois sistemas diferentes, e nenhuma transação cobre os dois:
- O commit no banco funciona, e o serviço cai antes de publicar → o pedido existe, mas ninguém nunca é avisado.
- O evento é publicado, e a transação do banco sofre rollback → os consumidores reagem a um pedido que não existe.
Transactional outbox
O transactional outbox2 elimina a escrita dupla. O serviço grava o evento em uma tabela outbox na mesma transação local do banco que o pedido. Um relay separado (um job de polling, ou CDC com Debezium) lê o outbox e publica no Kafka, marcando as linhas como enviadas.
sequenceDiagram
participant O as order-service
participant DB as Postgres
participant R as Relay do outbox
participant K as Kafka
O->>DB: BEGIN
O->>DB: INSERT INTO orders ...
O->>DB: INSERT INTO outbox (OrderCreated)
O->>DB: COMMIT (ambos ou nenhum)
R->>DB: lê as linhas não enviadas do outbox
R->>K: publica OrderCreated
R->>DB: marca a linha como enviada Se o relay cair depois de publicar, mas antes de marcar a linha, o evento é publicado de novo — mais um motivo para os consumidores serem idempotentes.
Sagas
Algumas operações de negócio atravessam vários serviços: fazer um pedido exige reservar estoque (product service) e cobrar o cliente (payment service). Cada serviço tem seu próprio banco, então uma única transação ACID é impossível (veja Microsserviços). Uma saga2 é uma sequência de transações locais, uma por serviço, em que cada passo é disparado pelo evento do passo anterior. Se um passo falha, a saga executa transações de compensação que desfazem semanticamente os passos já realizados (liberar o estoque, estornar o pagamento).
sequenceDiagram
participant O as Pedido
participant I as Estoque
participant P as Pagamento
O->>I: OrderCreated
I->>P: StockReserved
P->>I: PaymentFailed
I->>O: StockReleased (compensação)
O->>O: marca o pedido como CANCELLED | Estilo | Como é coordenado | Vantagem | Cuidado |
|---|---|---|---|
| Coreografia | Cada serviço reage aos eventos dos outros (diagrama acima) | Sem componente central, baixo acoplamento | O fluxo fica espalhado por muitos serviços; difícil de acompanhar e de mudar |
| Orquestração | Um orquestrador da saga envia comandos e espera as respostas | O fluxo fica explícito em um só lugar | O orquestrador é mais um serviço para construir e operar |
Sagas não são transações
Uma saga abre mão do isolamento: outras requisições podem ver o estado intermediário (estoque reservado, pagamento ainda não feito). Projete os estados explicitamente (PENDING, CONFIRMED, CANCELLED) e torne idempotentes todos os passos e todas as compensações.
Dead Letter Queue
Quando um consumidor falha repetidamente ao processar uma mensagem, ele não pode bloquear o resto do stream. Uma Dead Letter Queue (DLQ) é um tópico separado para onde as mensagens são movidas depois de esgotadas as tentativas de retry, permitindo que o stream principal continue enquanto as mensagens com falha são investigadas e reprocessadas manualmente.
stateDiagram-v2
[*] --> Pending
Pending --> Processing : consumidor pega a mensagem
Processing --> Committed : sucesso
Processing --> Retry : falha transitória (tentativa < máx.)
Retry --> Processing : após o backoff
Processing --> DLQ : máximo de retries excedido
DLQ --> Processing : reprocessamento manual Spring Kafka
A anotação @RetryableTopic do Spring Kafka cuida automaticamente do agendamento dos retries e do roteamento para a DLQ. Configure attempts, backoff e o nome do tópico da DLQ de forma declarativa, sem código repetitivo de tratamento de erros.
// default: 3 attempts in total; Spring Kafka 4 (Spring Boot 4) annotation
@RetryableTopic(backOff = @BackOff(delay = 1000, multiplier = 2))
@KafkaListener(topics = "order-events", groupId = "invoice-group")
public void onOrderCreated(OrderCreatedEvent event) {
invoiceService.create(event); // throws → retried after 1 s, then 2 s
}
@DltHandler
public void onDeadLetter(OrderCreatedEvent event) {
log.error("order {} sent to the dead letter topic", event.orderId());
}
Faça retry apenas das falhas que podem dar certo depois (um banco brevemente indisponível). Uma mensagem que nunca poderá ser processada — JSON inválido, um ID de produto desconhecido — deve ir direto para a DLQ; fazer retry dela só atrasa o resto da partição.
Quando usar (e quando não usar) eventos
| Cenário | Melhor opção | Motivo |
|---|---|---|
| Login / geração de token de autenticação | REST | Quem chama precisa do token de forma síncrona |
| E-mail de confirmação do pedido | Evento | O usuário não espera; o e-mail pode atrasar |
| Verificação de fraude em tempo real (precisa bloquear) | REST | É preciso a resposta antes de prosseguir |
| Log de auditoria de todas as alterações de pedidos | Evento | Fire-and-forget, fan-out |
| Processamento de pagamento | Depende | Se o retry idempotente for garantido: evento. Se for preciso confirmação bloqueante: REST + saga |
| Pipeline de dados de analytics | Evento | Alto volume, consumidores podem ser lentos |
| Health check de serviço | REST | Síncrono por natureza |
Complexidade operacional
Sistemas orientados a eventos são mais difíceis de depurar do que REST. Uma requisição que passa por 5 serviços e 3 tópicos do Kafka exige rastreamento distribuído (visto na aula de Observabilidade) para ser diagnosticada. Não adote a arquitetura orientada a eventos por si só — adote-a quando os custos de acoplamento e latência das chamadas síncronas virarem problemas concretos.
Erros comuns
- Supor exactly-once: duplicatas vão chegar; todo consumidor precisa ser idempotente.
- Escritas duplas: salvar no banco e publicar no Kafka sem um outbox perde ou inventa eventos após uma queda.
- Quebrar o esquema do evento: consumidores que você não conhece leem seus eventos. Apenas adicione campos opcionais; nunca renomeie nem remova campos sem uma nova versão (
OrderCreatedV2) — um schema registry com Avro ou Protobuf pode garantir isso. - Ignorar o lag do consumidor: o gráfico de profundidade da fila acima é real. Monitore o lag de cada grupo de consumidores (Kafka UI, Prometheus) e gere alertas quando ele crescer.
- Eventos que são comandos disfarçados:
SendEmailToCustomeracopla o produtor a um consumidor; publique o fato (OrderCreated) e deixe os consumidores decidirem.
Conexões
- Hands-on 9 — Kafka na loja:
OrderCreatedpublicado pelo order service e consumido pelo notification service, com uma DLQ. - Microsserviços — um banco por serviço, o motivo pelo qual sagas e outboxes são necessários.
- Sistemas distribuídos — consistência eventual, ordem e consenso (KRaft).
- Confiabilidade — retries, backoff e idempotência também se aplicam aos consumidores.
- Observabilidade e Hands-on 10 — rastreando uma requisição através dos tópicos.
Pontos-chave
- Use eventos quando quem chama não precisa da resposta para continuar; use uma requisição síncrona quando precisa.
- O Kafka é um log replicado e particionado: a ordem vale dentro de uma partição, a chave decide a partição e cada grupo de consumidores mantém seus próprios offsets — grupos diferentes fazem fan-out, o mesmo grupo divide o trabalho.
- Na prática, a entrega é at-least-once: faça o commit depois de processar e torne todo consumidor idempotente.
- Nunca faça escrita dupla: o transactional outbox publica eventos de forma confiável; sagas com compensações substituem transações distribuídas.
- Event sourcing e CQRS são poderosos em domínios específicos, não são o padrão; uma DLQ impede que mensagens envenenadas bloqueiem uma partição.
-
NARKHEDE, N.; SHAPIRA, G.; PALINO, T. Kafka: The Definitive Guide, 2nd ed. O'Reilly, 2021. ↩
-
RICHARDSON, C. Microservices Patterns — Event Sourcing, CQRS, Saga. ↩↩
-
FOWLER, M. Event Sourcing, 2005. ↩
-
YOUNG, G. CQRS Documents, 2010. ↩