Event-driven architecture na prática: quando eventos resolvem e quando complicam
1. Fundamentos da arquitetura orientada a eventos
A arquitetura orientada a eventos (EDA) difere fundamentalmente do modelo tradicional request-response. Enquanto em sistemas REST um serviço A chama diretamente o serviço B e aguarda uma resposta síncrona, na EDA um produtor publica um evento em um barramento e segue seu fluxo — os consumidores interessados reagem de forma assíncrona.
Os três elementos essenciais são:
- Produtores: emitem eventos sem conhecer os consumidores
- Consumidores: escutam eventos relevantes e agem de acordo
- Barramento de eventos: canal de comunicação que gerencia roteamento, persistência e entrega
Eventos são o padrão natural quando o domínio já opera em termos de ocorrências: "pedido criado", "pagamento confirmado", "estoque baixo". Sistemas de sensores IoT, notificações em tempo real e workflows de aprovação são exemplos clássicos onde eventos se encaixam perfeitamente.
# Exemplo: produtor publicando evento de pedido criado
{
"event_type": "order.created",
"aggregate_id": "order-12345",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"customer_id": "cust-789",
"items": [{"product_id": "p-001", "quantity": 2}],
"total": 150.00
}
}
2. Cenários onde eventos resolvem problemas reais
Desacoplamento entre serviços
Em um sistema de e-commerce, quando um pedido é criado, diversos serviços precisam agir: faturamento, estoque, logística, notificações. Com eventos, o serviço de pedidos simplesmente publica order.created. Cada serviço consome independentemente, sem acoplamento direto.
# Consumidor de estoque reage ao evento
consumer.subscribe("order.created", async (event) => {
const { items } = event.data;
for (const item of items) {
await inventoryService.reserve(item.product_id, item.quantity);
}
});
Sincronização entre bounded contexts
Em DDD, diferentes bounded contexts mantêm seus próprios modelos. Eventos permitem sincronizar estados sem violar limites. O contexto de pagamentos publica payment.confirmed, e o contexto de pedidos reage atualizando o status.
Reatividade em tempo real
Workflows longos como aprovação de crédito ou onboarding de clientes se beneficiam do processamento assíncrono. Cada etapa publica eventos que disparam a próxima fase, permitindo escalabilidade horizontal e resiliência.
3. Os problemas que eventos podem trazer
Complexidade de rastreamento
Em um sistema com 20 serviços e centenas de eventos, rastrear o fluxo completo de uma requisição torna-se desafiador. Ferramentas de tracing distribuído (como OpenTelemetry) são essenciais, mas exigem investimento.
# Header de correlação para rastreamento
{
"event_type": "payment.processed",
"correlation_id": "corr-abc-123",
"causation_id": "evt-order-created-456",
"data": { ... }
}
Garantias de entrega
Eventos entregues "pelo menos uma vez" exigem idempotência nos consumidores. "Exatamente uma vez" é mais complexo e geralmente envolve deduplicação com identificadores únicos.
# Consumidor idempotente
async function handlePaymentConfirmed(event) {
const processed = await checkDuplicate(event.event_id);
if (processed) return; // já processado
await processPayment(event.data);
await markProcessed(event.event_id);
}
Consistência eventual
Leituras imediatas após um evento podem retornar dados desatualizados. Em operações críticas (como saldo bancário), isso pode ser inaceitável. A solução é combinar eventos com leituras consistentes para dados sensíveis.
4. Padrões de implementação que funcionam
Event Sourcing
Armazena o histórico completo de mudanças como uma sequência de eventos. Permite reconstruir o estado atual e auditar todas as alterações.
# Eventos de uma conta bancária
[
{ "type": "account.created", "data": { "owner": "João", "balance": 0 } },
{ "type": "deposit.made", "data": { "amount": 1000 } },
{ "type": "withdrawal.made", "data": { "amount": 200 } }
]
Saga Pattern
Orquestra transações distribuídas com eventos de compensação. Se uma etapa falha, eventos de rollback desfazem as operações anteriores.
# Saga de pedido: se pagamento falha, libera estoque
saga.on("payment.failed", async (event) => {
await inventoryService.restock(event.data.items);
await notificationService.send("Pagamento recusado");
});
CQRS
Separa comandos (escrita) de consultas (leitura). Eventos atualizam modelos de leitura otimizados, reduzindo acoplamento e melhorando performance.
5. Armadilhas comuns
Eventos inflados
Eventos que carregam dados demais quebram contratos quando o modelo do produtor muda. A regra prática: carregue apenas o identificador e dados essenciais. Consumidores que precisam de mais dados devem consultar o produtor via API.
# Evento inflado (evite)
{ "type": "order.created", "data": { "customer_full_profile": {...}, "product_details": {...}, "payment_history": [...] } }
# Evento enxuto (prefira)
{ "type": "order.created", "data": { "order_id": "123", "customer_id": "456", "total": 150.00 } }
Dependências ocultas de ordem
Consumidores que assumem ordem específica de eventos (ex: "pagamento deve vir antes de envio") criam fragilidade. Solução: usar versões de agregados ou máquinas de estado que validam pré-condições.
Overengineering
Nem todo domínio precisa de eventos. Um CRUD simples com REST é mais eficiente e mais fácil de depurar. Forçar eventos onde não há necessidade real de desacoplamento ou assincronicidade adiciona complexidade sem benefício.
6. Ferramentas e infraestrutura para EDA madura
NATS JetStream
Barramento persistente que combina alta performance com garantias de entrega. Ideal para microsserviços que precisam de streaming confiável sem a sobrecarga operacional do Kafka.
# Configuração de stream no NATS JetStream
stream.add("orders", {
subjects: ["order.*"],
storage: "file",
max_msgs: 10000,
max_age: "72h"
});
XState
Biblioteca para modelar máquinas de estado complexas em consumidores. Garante que eventos sejam processados na ordem correta e que estados inválidos sejam rejeitados.
Monitoramento
Dead letter queues capturam eventos que falharam após múltiplas tentativas. Tracing distribuído com Jaeger ou Zipkin permite visualizar o fluxo completo.
# Configuração de dead letter queue
consumer.on("max_retries_exceeded", (event, error) => {
deadLetterQueue.send(event, {
error: error.message,
retries: 3,
timestamp: Date.now()
});
});
7. Checklist para decidir quando usar eventos
Perguntas-chave:
- O domínio já opera naturalmente com eventos? (pedidos, notificações, sensores)
- Você precisa de desacoplamento real entre serviços?
- A latência assíncrona é aceitável para o caso de uso?
- Você tem capacidade operacional para gerenciar a complexidade adicional?
Análise de trade-offs:
| Cenário | Eventos | REST |
|---|---|---|
| Latência | Maior (assíncrono) | Menor (síncrono) |
| Consistência | Eventual | Imediata |
| Complexidade | Alta | Baixa |
| Escalabilidade | Excelente | Limitada |
Exemplo prático: e-commerce
Use eventos para estoque: quando um pedido é criado, o evento order.created dispara a reserva de estoque assincronamente. Se o estoque acabar, um evento stock.exhausted notifica o cliente.
Evite eventos para relatórios simples: consultar o total de vendas do dia pode ser feito com uma query SQL direta. Adicionar eventos aqui só aumenta a complexidade sem benefício real.
# Decisão: estoque usa eventos porque precisa de reatividade e desacoplamento
if (domain.requiresReactivity && domain.benefitsFromDecoupling) {
useEDA();
} else {
useREST();
}
Referências
- Event-Driven Architecture: A Primer by Martin Fowler — Artigo fundamental que explica os conceitos básicos e padrões da arquitetura orientada a eventos
- NATS JetStream Documentation — Documentação oficial sobre o barramento persistente NATS JetStream, com exemplos de configuração e uso
- Saga Pattern in Microservices by Microsoft Azure — Guia prático sobre implementação do padrão Saga para transações distribuídas
- CQRS Pattern by Martin Fowler — Explicação detalhada sobre Command Query Responsibility Segregation e quando aplicá-lo
- Event Sourcing by Greg Young — Introdução ao Event Sourcing com exemplos práticos e casos de uso
- XState Documentation — Documentação oficial da biblioteca XState para modelagem de máquinas de estado em sistemas orientados a eventos
- Distributed Tracing with OpenTelemetry — Guia sobre tracing distribuído para rastreamento de fluxos de eventos em microsserviços