Event-Driven Architecture
Componentes se comunicam publicando e consumindo eventos, sem chamadas diretas entre si. Produtores não conhecem consumidores — e consumidores não conhecem produtores — tornando o sistema altamente desacoplado e extensível por adição, não por modificação.
Intenção
Substituir chamadas diretas entre componentes por mensagens assíncronas que descrevem o que aconteceu no sistema. Na Event-Driven Architecture (EDA), um componente que faz algo relevante publica um evento; qualquer componente interessado o consome e age de forma independente. Nenhum dos dois precisa saber da existência do outro.
Esse desacoplamento é a propriedade fundamental da EDA. Adicionar um novo comportamento ao sistema — enviar um e-mail quando um pedido é confirmado, atualizar um cache, incrementar uma métrica — significa adicionar um novo consumidor do evento existente, sem modificar o produtor. O princípio Open/Closed se manifesta aqui de forma natural.
EDA pode ser implementada dentro de um único processo (in-process, usando um event bus leve) ou entre serviços distribuídos (via brokers como Kafka, RabbitMQ, AWS SNS/SQS). A arquitetura é a mesma; a topologia e as garantias de entrega diferem. O termo mais preciso para o caso inter-processo é messaging ou message-driven architecture, mas EDA é o mais comum na literatura.
Problema
Em sistemas onde os componentes se chamam diretamente, o crescimento produz um conjunto de problemas que se intensificam com o tempo:
- Acoplamento temporal: o chamador precisa que o receptor esteja disponível no exato momento da chamada. Uma falha do receptor falha o chamador. Em sistemas distribuídos, isso cria cascatas de falha.
- Acoplamento de conhecimento: o componente que confirma um pedido precisa conhecer e chamar explicitamente o serviço de e-mail, o serviço de estoque, o serviço de fidelidade e o serviço de analytics. Cada novo requisito de negócio modifica o mesmo componente central.
- Dificuldade de extensão: adicionar um novo comportamento (ex.: notificar o parceiro logístico após a confirmação do pedido) exige modificar o código existente, com risco de regressão em funcionalidades não relacionadas.
- Escalabilidade síncrona: operações que poderiam ocorrer em paralelo (enviar e-mail e atualizar estoque são independentes entre si) são executadas sequencialmente porque o chamador espera a resposta de cada uma antes de prosseguir.
Estrutura
Na EDA, produtores publicam eventos em um broker (ou bus), e consumidores se registram para receber os tipos de evento que lhes interessam. O broker é o único ponto de conhecimento compartilhado — produtores e consumidores se conhecem apenas por meio do contrato do evento (seu schema).
PRODUTORES BROKER / BUS CONSUMIDORES
┌─────────────┐ ┌──────────────┐ ┌────────────────────┐
│ Serviço de │ ──publica──▶│ │──entrega──▶│ Serviço de E-mail │
│ Pedidos │ PedidoConf │ event bus │ └────────────────────┘
└─────────────┘ │ / broker │──entrega──▶┌────────────────────┐
│ │ │ Serviço de Estoque │
┌─────────────┐ ──publica──▶│ │ └────────────────────┘
│ Serviço de │ PagtoAprov └──────────────┘──entrega──▶┌────────────────────┐
│ Pagamentos │ │ Serviço Fidelidade │
└─────────────┘ └────────────────────┘
Contrato: apenas o schema do evento é compartilhado.
Produtores não conhecem consumidores.
Consumidores não conhecem produtores.
Consumidores não se conhecem entre si.
Os três padrões dentro de EDA
Martin Fowler distingue três variantes que diferem no que o evento carrega e no que o consumidor precisa fazer com ele. A maioria dos erros de projeto em EDA vem de confundir os três.
- Event Notification: o evento sinaliza que algo aconteceu. Ele carrega o mínimo necessário para identificar o fato — tipicamente o ID do agregado e o tipo de evento. O consumidor, se precisar de mais detalhes, precisa fazer uma chamada de volta ao produtor para buscá-los. É o padrão mais simples e o mais desacoplado em termos de payload — mas introduz uma chamada síncrona adicional quando o consumidor precisa dos dados.
- Event-Carried State Transfer (ECST): o evento carrega estado suficiente para que o consumidor possa agir sem precisar chamar o produtor de volta. O consumidor mantém uma cópia local dos dados que precisa — essencialmente um cache atualizado via eventos. Elimina a chamada síncrona adicional, mas aumenta o tamanho do payload e cria dependência no schema do evento: mudanças na estrutura do evento afetam todos os consumidores.
- Event Sourcing (ES): os eventos não são apenas notificação — são a fonte da verdade. O estado atual do sistema não é armazenado diretamente; ele é derivado aplicando a sequência de eventos em ordem. O banco de dados é substituído por um log imutável de eventos. ES é o mais poderoso e o mais complexo: auditoria perfeita, reconstrução de estado em qualquer ponto do tempo, mas exige lidar com versionamento de eventos, rehydration e projeções.
Onde a maioria erra: Event Notification e ECST são estratégias de comunicação — o estado do sistema ainda existe em algum banco. Event Sourcing é uma estratégia de persistência — o estado é derivado dos eventos. Usar "Event Sourcing" quando se quer dizer "Event Notification" é o equívoco terminológico mais comum em EDA.
Como funciona
O trecho abaixo mostra os três padrões side-by-side para o mesmo evento de domínio, ilustrando as diferenças no payload e no que o consumidor precisa fazer.
// ── Event Notification: payload mínimo ──────────────────────
interface PedidoConfirmadoNotification {
readonly tipo: 'PedidoConfirmado';
readonly pedidoId: string; // consumidor busca o resto se precisar
readonly ocorridoEm: string;
}
// ── Event-Carried State Transfer: payload suficiente ─────────
interface PedidoConfirmadoECST {
readonly tipo: 'PedidoConfirmado';
readonly pedidoId: string;
readonly clienteEmail: string; // consumidor não precisa buscar
readonly total: number;
readonly itens: { produtoId: string; quantidade: number }[];
readonly ocorridoEm: string;
}
// ── Event Sourcing: evento É o estado ────────────────────────
// O agregado é reconstruído aplicando eventos em ordem
class PedidoAggregate {
private id = '';
private status = 'novo';
private itens: Item[] = [];
apply(event: DomainEvent): void {
if (event.tipo === 'PedidoConfirmado') {
this.status = 'confirmado';
}
// cada evento muta o estado do agregado
}
}
// ── Publicação (produtores não conhecem consumidores) ─────────
class ServiçoPedidos {
confirmar(pedidoId: string): void {
// ... lógica de negócio ...
this.eventBus.publish({ tipo: 'PedidoConfirmado', pedidoId, ocorridoEm: new Date().toISOString() });
// não chama ServiçoEmail diretamente
}
}
Quando usar
- Integração entre serviços independentes: quando múltiplos serviços precisam reagir ao mesmo fato de negócio sem que nenhum deles seja o orquestrador central, EDA é a forma mais natural de integração. Cada serviço reage de forma autônoma, com seu próprio ritmo e suas próprias garantias.
- Extensibilidade por adição: quando o requisito de negócio é "adicionar mais reações ao mesmo fato" com frequência, EDA é mais sustentável do que chamadas diretas. Adicionar um consumidor não modifica o produtor nem os consumidores existentes.
- Desacoplamento de disponibilidade: quando o produtor não pode esperar que todos os consumidores estejam disponíveis no momento da publicação — filas garantem que o evento será processado quando o consumidor estiver pronto.
- Auditoria e rastreabilidade: o log de eventos é uma trilha de auditoria natural de tudo o que aconteceu no sistema, com timestamp e payload completo de cada ocorrência.
Quando evitar
- Fluxos que exigem resposta imediata e síncrona: se a requisição do usuário precisa de uma resposta calculada com dados de múltiplos serviços, uma chamada síncrona (RPC, HTTP) é mais simples e direta. Tentar simular sincronia com EDA (correlation ID, reply queue) adiciona complexidade sem benefício equivalente.
- Lógica de negócio simples em um único serviço: dentro de um monolito com lógica de negócio simples, um event bus interno adiciona indireção desnecessária. Chamadas diretas entre classes ou módulos são mais legíveis e depuráveis.
- Equipes sem experiência com sistemas assíncronos: debugar falhas em sistemas event-driven — rastrear por que um evento não foi processado, investigar ordering, tratar duplicatas — exige ferramentas e experiência específicas. A curva de aprendizado operacional é significativa.
Prós e contras
Prós
- Desacoplamento estrutural: produtores e consumidores evoluem de forma independente — adicionar um novo consumidor não toca no produtor.
- Extensibilidade por adição: novos comportamentos surgem de novos consumidores, sem modificar código existente.
- Resiliência: filas de mensagens absorvem picos e permitem que consumidores lentos ou temporariamente indisponíveis processem eventos no seu ritmo.
- Rastreabilidade: o log de eventos é uma auditoria nativa de todas as mudanças do sistema.
- Escalabilidade independente: produtores e consumidores escalam separadamente, ajustando ao volume de cada um.
Contras
- Debugging difícil: rastrear o fluxo causal de uma operação exige correlacionar eventos através de múltiplos serviços, logs e timestamps — ferramentas de observabilidade (distributed tracing) são essenciais.
- Consistência eventual: o sistema fica consistente com o tempo, não instantaneamente. Fluxos que exigem consistência imediata precisam de workarounds.
- Complexidade operacional: brokers de mensagem (Kafka, RabbitMQ) adicionam infraestrutura que precisa ser configurada, monitorada e operada.
- Ordering e idempotência: garantir que eventos sejam processados na ordem correta e exatamente uma vez (ou que o consumidor seja idempotente) é não-trivial em sistemas distribuídos.
- Schema acoplado: consumidores dependem do schema dos eventos. Mudanças quebram consumidores — exigindo versionamento de schema, compatibilidade backward/forward ou um registro de schema central.
Armadilhas comuns
1. Confundir EDA com o padrão Observer
Observer (GoF) e EDA compartilham a ideia de publicar e consumir notificações, mas diferem em escopo, sincronia e topologia. Observer é in-process: o sujeito chama diretamente os métodos dos observadores registrados, no mesmo processo, tipicamente de forma síncrona. EDA é inter-process: produtores publicam mensagens em um broker externo, e consumidores as processam de forma assíncrona, possivelmente em processos distintos, em máquinas distintas. No Observer, o sujeito conhece a interface dos observadores; em EDA, o produtor não conhece nenhum consumidor.
Resumo prático: Observer = mesmo processo, chamada direta, possivelmente síncrona. EDA = entre processos/serviços, mediado por broker, assíncrono por design.
2. Debugging sem observabilidade
Em um sistema síncrono, um stack trace conta a história completa de uma falha. Em EDA, um evento publicado pelo serviço A é consumido pelo serviço B, que publica outro evento consumido pelo serviço C. Se C falha, a causa pode estar em A. Sem distributed tracing (OpenTelemetry, Jaeger, Zipkin) e sem um campo de correlation ID propagado em todos os eventos, investigar uma falha se torna uma tarefa de arqueologia em múltiplos logs. Observabilidade não é opcional em EDA — é parte da arquitetura.
3. Ignorar ordering e idempotência
Brokers de mensagem geralmente garantem entrega pelo menos uma vez (at-least-once delivery), não exatamente uma vez. Eventos podem ser entregues em ordem diferente da publicação e podem ser entregues mais de uma vez. Consumidores que não são idempotentes produzem efeitos colaterais duplicados. Consumidores que assumem ordering podem processar um PedidoCancelado antes do PedidoConfirmado. Projete consumidores idempotentes desde o início — não como otimização posterior.
4. Acoplamento via schema de evento sem versionamento
O schema do evento é o contrato entre produtor e consumidores. Mudanças
retrocompatíveis (adicionar um campo opcional) são seguras. Mudanças
incompatíveis (remover um campo, renomear um tipo) quebram todos os
consumidores simultaneamente. Em sistemas em produção, isso exige estratégias
de versionamento: campos opcionais com valor default, eventos versionados
(PedidoConfirmadoV2), transformadores de versão ou um schema
registry com validação de compatibilidade.
5. Event sprawl: eventos sem governança
Sem governança, o catálogo de eventos cresce de forma descontrolada: eventos com nomes inconsistentes, payloads sobrepostos, duplicatas semânticas e responsabilidades ambíguas. Com o tempo, ninguém sabe quais eventos existem, quem os produz e quem os consome. Um registro de eventos (event catalog) com owner explícito, schema documentado e histórico de versões é parte essencial da governança de EDA em escala.
Arquiteturas e padrões relacionados
O Observer (GoF) é o ancestral conceitual da EDA, mas opera em um escopo radicalmente diferente. No Observer, o sujeito mantém uma lista de observadores e os notifica chamando diretamente seus métodos no mesmo processo. Em EDA, a comunicação é mediada por um broker externo, assíncrona e sem referência direta. Um event bus in-process (como o EventEmitter do Node.js ou um SimpleEventBus) é tecnicamente um Observer — EDA começa quando a comunicação cruza a fronteira do processo.
CQRS e EDA se combinam com frequência: o CommandHandler do lado de escrita publica um evento de domínio após persistir uma mudança; esse evento alimenta o Read Model do lado de leitura. CQRS resolve o problema de modelos distintos para leitura e escrita; EDA resolve o problema de como propagar essas mudanças de forma desacoplada. Nenhum implica o outro.
Em Microsserviços, EDA é frequentemente a cola que mantém os serviços desacoplados temporalmente e de disponibilidade. Cada serviço publica eventos de domínio que outros serviços consomem para manter seus próprios dados locais atualizados — eliminando chamadas síncronas entre serviços para leituras frequentes.