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