Pular para conteúdo

Visão geral

São 09:59:58 e um show em estádio está prestes a abrir as vendas no BoxOffice. Às 10:00, dois milhões de fãs apertam "Comprar" para sessenta mil lugares. Por trás desse botão, a primeira versão do BoxOffice faz tudo em uma única requisição síncrona: reserva os lugares, cobra o cartão, envia o e-mail, manda uma notificação push para o celular, atualiza o índice de busca e registra a venda para o analytics. O provedor de e-mail está um pouco lento hoje (800 ms em vez de 80 ms), então cada compra passa a levar um segundo a mais, o serviço order fica sem threads e os fãs que estavam prestes a pagar veem um spinner e depois um erro. Nada estava quebrado — o sistema simplesmente foi ligado de um jeito em que cada passo precisava terminar antes que qualquer usuário recebesse uma resposta.

Esta aula é sobre a outra forma de ligar os serviços: mensagens. Em vez de chamar cada serviço e esperar, um serviço escreve uma mensagem em um broker e segue em frente; quem precisa dela a lê quando puder. Bem feita, a mensageria absorve picos, desacopla times e tira serviços lentos ou com falha do caminho crítico. Feita sem cuidado, perde pedidos, duplica e-mails e esconde falhas em uma fila que ninguém observa. Vamos aprender os dois lados.

A história até aqui

Capítulo 8 da história da disciplina. A borda espalha os fãs por réplicas sem estado (aula 4, aula 5), o PostgreSQL vende cada assento uma vez só (aula 6) e o Redis responde as páginas de evento (aula 7). O caminho de escrita ainda é uma única cadeia síncrona, e a multidão ainda é 30 vezes grande demais: chegam cerca de 33.000 reservas/s para as ~1.100 que um banco aguenta. Esta aula responde: o que pode sair da requisição e esperar numa fila, como nunca perder nem duplicar um evento no caminho, e como dar a 2 milhões de fãs uma fila justa?

Objetivos de aprendizagem

Ao final desta aula você deve ser capaz de:

  • Decidir se uma interação entre dois serviços do BoxOffice deve ser uma chamada síncrona, um comando enfileirado ou um evento publicado, e justificar a escolha com argumentos de latência, disponibilidade e acoplamento.
  • Comparar filas de mensagens, tópicos pub/sub e logs de eventos, e escolher o componente certo da paleta (queue, pubsub, stream, kafka) para um requisito.
  • Explicar como o Kafka armazena e entrega dados — tópicos, partições, offsets, consumer groups, replicação, retenção e compactação — e dimensionar o número de partições para um throughput-alvo.
  • Estimar backlog, consumer lag e tempo de escoamento sob um pico de tráfego, e projetar backpressure para uma fila.
  • Explicar entrega at-most-once, at-least-once e exactly-once, e implementar um consumidor idempotente e uma dead letter queue.
  • Aplicar transactional outbox, change data capture, sagas (coreografia e orquestração), CQRS e event sourcing para manter serviços consistentes sem transações distribuídas.
  • Evoluir o schema de um evento sem quebrar os consumidores.

Roteiro

  1. Esta visão geral — síncrono vs assíncrono, o que um broker oferece (e quanto custa), nivelamento de carga em números, o vocabulário e os quatro componentes de mensageria da paleta.
  2. Filas e pub/sub — filas ponto a ponto, consumidores concorrentes, confirmações, backpressure, a sala de espera da flash sale e o fan-out do pub/sub.
  3. Kafka e logs de eventos — a abstração de log, partições, chaves e ordenação, consumer groups, replicação, retenção, compactação e dimensionamento de partições.
  4. Semântica de entrega — at-most/at-least/exactly-once, consumidores idempotentes, ordenação, retries e dead letter queues, evolução de schema.
  5. Padrões — transactional outbox, change data capture, sagas, CQRS e event sourcing, todos aplicados à compra no BoxOffice.
  6. Quiz — teste seu entendimento.

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


Síncrono vs assíncrono

Em uma interação síncrona, quem chama envia uma requisição e espera a resposta antes de continuar — uma chamada HTTP ou gRPC. Em uma interação assíncrona, quem chama entrega uma mensagem a um intermediário (um broker) e continua imediatamente; o receptor processa a mensagem depois, possivelmente muito depois, possivelmente em outro continente.

Compare as duas versões da compra no BoxOffice. O usuário clica em "Comprar"; o serviço order precisa criar o pedido, e outros cinco serviços precisam ficar sabendo dele.

sequenceDiagram
    autonumber
    participant U as Fã
    participant O as order
    participant N as notification
    participant S as search
    participant A as analytics
    U->>+O: POST /orders
    O->>+N: envia e-mail + push
    N-->>-O: ok (800 ms hoje)
    O->>+S: atualiza "lugares restantes"
    S-->>-O: ok
    O->>+A: registra a venda
    A-->>-O: ok
    O-->>-U: 201 Created (depois de todos eles)

O tempo de resposta é a soma de todas as chamadas, e a compra falha se qualquer uma delas falhar.

sequenceDiagram
    autonumber
    participant U as Fã
    participant O as order
    participant K as Kafka
    participant N as notification
    participant S as search
    participant A as analytics
    U->>+O: POST /orders
    O->>K: publica OrderConfirmed
    O-->>-U: 201 Created
    K-->>N: OrderConfirmed
    K-->>S: OrderConfirmed
    K-->>A: OrderConfirmed

O tempo de resposta é o tempo de salvar o pedido e publicar uma mensagem. Cada consumidor lê o evento de forma independente, no seu próprio ritmo.

A diferença não é só velocidade. Ela aparece em três números que você já sabe calcular (veja Estimativas):

Propriedade Cadeia síncrona Eventos assíncronos
Latência vista pelo fã \(t_{order} + t_{notif} + t_{search} + t_{analytics}\) \(t_{order} + t_{publish}\)
Disponibilidade do "Comprar" \(A_{order} \times A_{notif} \times A_{search} \times A_{analytics}\) \(A_{order} \times A_{broker}\)
Acoplamento order conhece cada consumidor e sua API order conhece o nome de um tópico e o schema do evento
Consistência tudo está feito quando o usuário vê "201" os consumidores alcançam depois (consistência eventual)
Falha de um consumidor a compra falha o backlog do consumidor cresce; a compra continua funcionando
Depuração uma requisição, uma pilha de chamadas um fluxo espalhado por tópicos e serviços: exige tracing

BoxOffice: disponibilidade do botão Comprar

Suponha que order tenha 99,95% de disponibilidade e cada um dos três serviços seguintes, 99,9%. De forma síncrona, a compra precisa dos quatro:

\[ A = 0.9995 \times 0.999^3 \approx 99.65\% \]

São cerca de 2,5 horas de compras com falha por mês. Com eventos, a compra precisa de order e do broker (um cluster Kafka replicado, digamos 99,95%):

\[ A = 0.9995 \times 0.9995 \approx 99.90\% \]

Os serviços seguintes ainda podem falhar — mas agora a falha deles atrasa um e-mail em vez de perder uma venda.

Pense antes: a reserva de lugares também deveria ser assíncrona?

Não. Quando o fã clica em um lugar, ele precisa saber agora se o lugar é dele. Uma reserva é uma pergunta cuja resposta quem chama precisa para continuar, e ela precisa ser fortemente consistente (nada de venda duplicada). Ela continua sendo uma chamada síncrona ao inventory (veja Transações). Assincronia é para trabalho cujo resultado quem chama não precisa para continuar: notificações, atualização da busca, analytics, notas fiscais.

A regra prática: se quem chama precisa da resposta para continuar, chame; se só precisa que o trabalho aconteça em algum momento, envie uma mensagem.


O que um broker oferece

Um message broker é um servidor que recebe mensagens de produtores, as armazena de forma durável e as entrega a consumidores1. Colocar um broker entre dois serviços traz quatro propriedades que uma chamada direta não traz:

  1. Desacoplamento temporal. Produtor e consumidor não precisam estar no ar ao mesmo tempo. Se o notification está sendo reimplantado, os eventos OrderConfirmed esperam no broker.
  2. Nivelamento de carga. O broker é um buffer: produtores podem escrever mais rápido do que os consumidores leem por um tempo, e o backlog escoa depois. Um pico vira uma fila em vez de uma indisponibilidade.
  3. Fan-out. Um evento publicado pode ser entregue a muitos consumidores independentes. Adicionar um novo consumidor (detecção de fraude, programa de fidelidade) não mexe no produtor.
  4. Escala independente e isolamento de falhas. Cada consumidor escala com a própria carga e falha sozinho. Um pipeline de analytics lento não atrasa os e-mails.

Nada é de graça. O preço da assincronia:

  • Consistência eventual. Por um tempo, a página de busca pode ainda mostrar "120 lugares restantes" quando já são 80. O sistema precisa ser projetado (e a interface, redigida) para isso2.
  • Duplicatas e reordenação. A maioria dos brokers entrega at least once: consumidores vão ver a mesma mensagem duas vezes e precisam ser idempotentes (veja Semântica de entrega).
  • Depuração mais difícil. Uma compra agora é uma cadeia de eventos entre tópicos; sem tracing distribuído e métricas de consumer lag, as falhas se escondem em um backlog (veja Observabilidade).
  • Mais um sistema crítico. O broker precisa ser replicado, monitorado, atualizado e ter sua capacidade planejada.

Nivelamento de carga em números

A flash sale é o caso clássico de nivelamento de carga. As requisições de compra dos fãs chegam muito mais rápido do que o inventory consegue processar reservas, mas só por alguns minutos. Se o order chamasse o inventory diretamente, o excesso viraria timeouts e erros. Com uma fila no meio, o excesso vira backlog — mensagens esperando — e o único custo é tempo de espera.

A simulação abaixo usa números ilustrativos: 200 requisições/s antes das 10:00, um pico de 6.000 requisições/s por três minutos e depois uma multidão que diminui devagar. Os workers processam 2.000 ou 4.000 requisições/s.

2026-11-01T21:55:04.910718 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ 0 5 10 15 20 minutes after 09:58 0 1000 2000 3000 4000 5000 6000 requests per second Arrivals vs consumer capacity arrivals (requests/s) worker capacity 2000/s worker capacity 4000/s 0 5 10 15 20 minutes after 09:58 0 100 200 300 400 500 600 700 backlog (thousand messages) Queue backlog 2000/s: peak 741k, wait 6.2 min 4000/s: peak 360k, wait 1.5 min

Três coisas para ler no gráfico:

  • O backlog cresce na diferença das taxas. Durante o pico ele cresce \(6000 - 2000 = 4000\) mensagens por segundo com o pool menor de workers: em três minutos, \(4000 \times 180 = 720{,}000\) mensagens.
  • A espera é o backlog dividido pelo throughput — a Lei de Little de novo (veja Filas): \(720{,}000 / 2000 = 360\) s, seis minutos para o último fã da fila. Dobrar os workers reduz o pico pela metade e o escoa muito mais rápido.
  • O backlog só escoa quando as chegadas caem abaixo da capacidade. Se a taxa média de chegada fica acima da capacidade, nenhuma fila salva você: o backlog cresce para sempre. Uma fila absorve picos, não uma falta permanente de capacidade.

Experimente no playground

O cenário async-queue coloca uma fila e um pool de workers atrás de um servidor de aplicação. Aperte Run com o perfil spike e observe a profundidade da fila e o consumer lag. Depois (1) reduza o número de workers e veja o backlog crescer e o atraso de ponta a ponta subir enquanto a latência da API continua estável; (2) aumente os workers até o backlog escoar em até um minuto depois do pico. Abra o playground completo para adicionar uma regra de autoscaling pela profundidade da fila.


O vocabulário

Mensageria tem muitas palavras para coisas parecidas. Estas são as que usamos no curso.

Termo Significado Exemplo no BoxOffice
Mensagem Qualquer unidade de dados enviada por um broker: cabeçalhos + payload Qualquer um dos abaixo
Comando Um pedido para que um receptor específico faça algo; pode ser rejeitado GenerateTicketPdf { orderId } na fila ticket-pdf
Evento Um fato imutável sobre algo que já aconteceu; o produtor não sabe quem reage OrderConfirmed { orderId, eventId, seats, total }
Produtor / publisher O serviço que escreve mensagens order
Consumidor / subscriber O serviço que as lê notification, search, analytics
Broker O servidor (cluster) que armazena e entrega mensagens Kafka, RabbitMQ, Amazon SQS
Fila Um canal em que cada mensagem é processada por um consumidor ticket-pdf
Tópico Um canal em que cada mensagem é entregue a toda assinatura order-events
Backlog / consumer lag Mensagens escritas mas ainda não processadas "o notification está 42.000 mensagens atrasado"
Confirmação (ack) O consumidor dizendo ao broker "feito, pode esquecer/avançar" basicAck, commit de offset

Nomeie eventos no passado

Um evento é um fato: OrderConfirmed, SeatsReleased, PaymentFailed. Um nome no imperativo — SendConfirmationEmail — é um comando disfarçado: acopla o produtor a um consumidor e a uma ação. Publique o que aconteceu e deixe cada consumidor decidir o que fazer com isso.

O que vai dentro de um evento

Martin Fowler distingue várias coisas que as pessoas querem dizer com "orientado a eventos"3. Duas delas mudam a forma de projetar o payload:

O evento carrega pouco mais que um identificador. Consumidores que precisam de detalhes chamam o produtor de volta.

{ "type": "OrderConfirmed", "eventId": "e-9f2c", "orderId": "o-7731", "occurredAt": "2027-03-14T10:00:07Z" }

Pequeno e estável, mas cada consumidor gera carga no order (e depende de ele estar no ar) para buscar os detalhes.

O evento carrega os dados de que os consumidores precisam, então eles nunca chamam de volta.

{
  "type": "OrderConfirmed", "eventId": "e-9f2c", "orderId": "o-7731",
  "occurredAt": "2027-03-14T10:00:07Z",
  "customer": { "id": "c-12", "email": "ana@example.com", "locale": "pt-BR" },
  "event": { "id": "ev-88", "name": "Stadium Show", "startsAt": "2027-05-01T21:00:00-03:00" },
  "seats": ["B-12", "B-13"], "total": { "amount": 780.00, "currency": "BRL" }
}

Os consumidores ficam totalmente desacoplados da disponibilidade do order, ao preço de um payload maior, de um schema mais difícil de mudar e de dados pessoais (o e-mail) copiados para todos os consumidores.

Os outros dois significados — event sourcing e CQRS — são padrões de arquitetura tratados em Padrões.


A paleta de mensageria

O System Design Playground tem quatro componentes de mensageria. Não são quatro marcas; são quatro formas de comunicação. O catálogo de componentes tem os detalhes de cada um.

Componente Forma Quem recebe uma mensagem Depois de consumida Ordenação Produtos típicos
queue Message Queue ponto a ponto um consumidor do pool apagada em geral nenhuma (filas FIFO: por grupo) Amazon SQS, RabbitMQ, filas do Azure Service Bus, Google Cloud Tasks
pubsub Pub/Sub fan-out toda assinatura (uma cópia para cada) apagada por assinatura em geral nenhuma (chaves de ordenação opcionais) Google Cloud Pub/Sub, Amazon SNS, tópicos do Azure Service Bus, NATS, Redis Pub/Sub
stream Event Stream log particionado (gerenciado) todo consumer group, cada um na sua posição mantida até a retenção expirar por partição / shard Amazon Kinesis Data Streams, Azure Event Hubs, Redis Streams
kafka Kafka log particionado e replicado todo consumer group, cada um na sua posição mantida até a retenção expirar (ou compactada) por partição Apache Kafka, Confluent Cloud, Amazon MSK, Redpanda

A pergunta decisiva é o que acontece com uma mensagem depois que ela é lida:

  • Em uma fila, ler é tirar: a mensagem some quando confirmada. Ótimo para distribuir trabalho.
  • Em um log, ler é olhar: a mensagem fica, e cada leitor guarda um marcador. Ótimo para muitos leitores independentes, replay e histórico.
flowchart TD
    q1{"Quem chama precisa da<br/>resposta para continuar?"}
    q1 -->|sim| sync["Chamada síncrona<br/>(REST / gRPC) + timeout"]
    q1 -->|não| q2{"Quantos serviços<br/>precisam reagir?"}
    q2 -->|"exatamente um<br/>(um trabalho a fazer)"| queue["queue<br/>consumidores concorrentes"]
    q2 -->|"muitos, cada um com sua cópia"| q3{"Precisa de replay, ordem<br/>por chave ou alto throughput?"}
    q3 -->|não| pubsub["pubsub<br/>tópico + assinaturas"]
    q3 -->|sim| log["stream / kafka<br/>log particionado"]

Quando não usar assincronia

Interação no BoxOffice Melhor escolha Por quê
Fazer login, obter um token Síncrona O usuário precisa do token agora
Reservar os lugares B-12 e B-13 Síncrona O usuário precisa saber agora; precisa ser fortemente consistente
Autorizar o cartão Síncrona com chave de idempotência, ou assíncrona com página de status O usuário espera o resultado, mas o provedor pode ser lento (veja Integrações)
Entrar na sala de espera da abertura de vendas Fila Admitir fãs na taxa que o inventory aguenta
Enviar e-mail e push depois da compra Evento (pubsub ou kafka) O usuário não espera; pode atrasar alguns segundos
Atualizar "lugares restantes" na busca Evento / change data capture Consistência eventual é aceitável para navegação
Gerar o ingresso em PDF Fila (comando) Um worker deve fazer isso uma vez; retries são aceitáveis
Alimentar o data warehouse Event stream Alto volume, com replay, muitos leitores

O mapa de conceitos de mensageria

Toda mensagem percorre o mesmo caminho: um produtor a entrega a um dos quatro componentes de mensageria, e um consumidor a processa depois. O mapa liga cada componente da paleta (verde) aos conceitos desta aula (azul) e às aulas que os continuam:

flowchart LR
    order["app (order)<br/>produtor"]:::comp --> queue["queue"]:::comp
    order --> pubsub["pubsub"]:::comp
    order --> log["stream · kafka"]:::comp
    queue --> worker["worker"]:::comp
    pubsub --> cons["notifications · search ·<br/>analytics"]:::comp
    log --> cons
    order -.- c0["síncrono vs assíncrono, outbox,<br/>sagas, CQRS, event sourcing"]:::tech
    queue -.- c1["consumidores concorrentes, ack,<br/>backpressure, sala de espera"]:::tech
    pubsub -.- c2["fan-out, assinaturas,<br/>push/pull, filtros"]:::tech
    log -.- c3["partições, chaves, offsets,<br/>consumer groups, retenção"]:::tech
    worker -.- c4["pelo menos uma vez, consumidor idempotente,<br/>ordenação, DLQ, schemas"]:::tech
    c3 -.- d1["consenso, KRaft<br/>(aula 3)"]
    c4 -.- d2["retries, SLO de atualidade<br/>(aulas 9 e 10)"]
    c4 -.- d3["consumer lag, tracing<br/>(aula 9)"]
    classDef comp fill:#1e9e5a22,stroke:#1e9e5a
    classDef tech fill:#2f6fed22,stroke:#2f6fed
Componente da paleta Pergunta que responde Conceitos Ensinado em Cenários
app Chamar e esperar, ou enviar uma mensagem? síncrono vs assíncrono, disponibilidade de uma cadeia, consistência eventual, comandos vs eventos esta visão geral async-queue
queue Quem faz este trabalho, e em que taxa? consumidores concorrentes, ack e visibility timeout, backpressure, sala de espera Filas e pub/sub, a sala de espera async-queue, flash-sale
pubsub Como cada serviço interessado recebe a sua cópia? tópico, assinaturas, push/pull, filtros, durável vs efêmero Filas e pub/sub notification-system, chat
stream · kafka Quem precisa de histórico, replay e ordem por chave? log, partições, chaves, consumer groups, lag, replicação, retenção, compactação Kafka e logs de eventos event-fanout
worker · notifications · search · analytics E se uma mensagem chegar duas vezes, atrasada, fora de ordem ou quebrada? pelo menos uma vez, consumidor idempotente, atualizações guardadas por versão, DLQ, evolução de schema Semântica de entrega notification-system, payment-system
sql + kafka · payment Como altero meus dados e publico, entre serviços, sem 2PC? transactional outbox, CDC, sagas, CQRS, event sourcing Padrões payment-system, flash-sale

Na entrevista

Entrevistadores prestam atenção no porquê de você adicionar uma fila. "Vou colocar Kafka aqui", sozinho, é um sinal ruim; "o e-mail não precisa bloquear a compra, então o order publica OrderConfirmed e o notification consome — isso tira o provedor de e-mail da cadeia de disponibilidade da compra e absorve o pico das 10:00 como backlog" é um sinal forte. Sempre continue com as consequências que você vai tratar: duplicatas (consumidor idempotente), ordenação (chave por orderId), falhas (retries + DLQ) e monitoramento (consumer lag)4.

Boas práticas

  • Decida síncrono ou assíncrono por chamada perguntando "o usuário precisa desta resposta agora?" — a reserva do assento e a autorização do pagamento ficam na requisição; o e-mail, o PDF do ingresso, a busca e o analytics viram eventos. No BoxOffice: o POST /orders responde depois de dois inserts e um commit (lab 7 — outbox).
  • Publique fatos do seu domínio, não comandos para um serviço conhecido — OrderPlaced, não SendEmail: busca ou analytics assinam com seu próprio grupo de consumidores e o order não muda. No BoxOffice: o notification lê order-events como grupo notification.
  • Dê a toda mensagem um envelope: um eventId único criado uma vez, um eventType, o momento em que aconteceu e uma versão de schema — consumidores deduplicam pelo id e roteiam pelo tipo. No BoxOffice: o relay copia eventId e eventType para cabeçalhos do Kafka (quadro do outbox).
  • Use como chave de cada mensagem a entidade cuja ordem importa — todos os eventos de um pedido caem numa partição, em ordem, enquanto pedidos diferentes se espalham por todas as partições. No BoxOffice: o relay usa o id do pedido como chave no Kafka.
  • Dimensione consumidores por taxa de chegada × tempo de serviço, com folga, e calcule o tempo de escoamento antes da abertura de vendas — 100 pedidos/s a 50 ms cada exigem pelo menos 5 threads; com uma thread, um pico de 60 s deixa ~4.800 mensagens e ~240 s de atraso (lab 7 — pico e escoamento).

Armadilhas comuns

  • Assincronia para tudo. Transformar uma pergunta que precisa de resposta ("este lugar está livre?") em mensagem adiciona latência, complexidade e um loop de polling, e não ganha nada.
  • Uma fila como plano de capacidade. Uma fila absorve picos; ela não conserta um consumidor permanentemente mais lento que o produtor. Observe a tendência do backlog.
  • Ninguém observa o backlog. Uma fila sem monitoramento transforma uma indisponibilidade em um atraso silencioso: a API está verde enquanto os e-mails estão três horas atrasados. Alerte sobre o consumer lag e sobre a idade da mensagem mais antiga.
  • Supor exactly-once. Planeje para duplicatas desde o primeiro dia.
  • Escrita dupla. Salvar no banco e depois publicar no broker perde ou inventa eventos depois de um crash; use o transactional outbox.

O que quebra em seguida

O e-mail saiu da requisição: no lab 7, uma falha no e-mail não derruba mais nenhum pedido, e a sala de espera só deixa entrar o que o inventory consegue atender. O preço é a visibilidade. Uma compra agora é uma requisição, uma linha no banco, um relay do outbox, um registro no Kafka e um consumidor, em quatro processos e em quatro momentos diferentes: quando um fã diz "paguei e não recebi o e-mail", ninguém consegue dizer onde o pedido parou. E quando o provedor de pagamento fica lento, as médias nos dashboards continuam verdes enquanto os fãs falham no checkout. Seguir um pedido entre serviços, e medir o que os fãs de fato sentem, é a aula 9 — Observabilidade.



  1. HOHPE, G.; WOOLF, B. Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Addison-Wesley, 2003. Veja também enterpriseintegrationpatterns.com. ↩

  2. KLEPPMANN, M.; RICCOMINI, C. Designing Data-Intensive Applications, 2. ed. O'Reilly. Capítulo sobre processamento de streams. ↩

  3. FOWLER, M. What do you mean by "Event-Driven"?. martinfowler.com, 2017. ↩

  4. XU, A. System Design Interview — An Insider's Guide, vol. 2. ByteByteGo, 2022. Capítulo "Distributed Message Queue". ↩