Event-driven Architecture: você está reaproveitando eventos ou mentindo para o seu próprio sistema?

Quantos eventos no seu sistema são emitidos em situações onde o fato que eles representam simplesmente não aconteceu?

Um PedidoCriado disparado por uma rotina de correção. Um PagamentoConfirmado emitido por um job de reprocessamento. Um UsuarioCadastrado lançado por uma migração de base legada. O fato não aconteceu, mas o evento foi emitido assim mesmo — porque “já tem tudo plugado”.

Essa é a confusão mais destrutiva em event-driven architecture. Não é sobre nomenclatura. É sobre semântica. E quando a semântica quebra, o sistema inteiro passa a operar sobre premissas falsas.

Eventos e Comandos se parecem, mas representam coisas opostas

Um evento e um comando:

  • podem ter o mesmo payload
  • podem trafegar pela mesma infraestrutura
  • podem até ter nomes parecidos

Mas a intenção que cada um carrega é fundamentalmente diferente.

Um evento é a notificação de um fato. Algo aconteceu. PedidoCriado significa que um pedido foi criado. Um passado consumado, irreversível.

O produtor do evento

  • não sabe quem consome
  • não espera uma reação específica
  • não tem responsabilidade sobre o que acontece depois

Ele apenas registra que um fato ocorreu.

Um comando é um pedido de ação. Algo precisa acontecer. SepararEstoque significa que alguém está solicitando ao serviço de estoque que execute uma operação. O emissor sabe para quem está enviando, espera uma execução, e assume responsabilidade pela intenção.

Essa distinção parece óbvia quando escrita assim. Na prática, a infraestrutura conspira contra ela. O RabbitMQ, o Kafka, o SNS, todos tratam eventos e comandos como mensagens. A ferramenta nivela o que a arquitetura deveria separar. E quando tudo é “mensagem”, a distinção semântica se perde no vocabulário do dia a dia do time.

O handler de evento é o adaptador que absorve acoplamento

Entre o evento e o comando existe uma peça que raramente recebe a atenção que merece: o handler de evento.

O handler é quem traduz fato em intenção.

Ele recebe PedidoCriado e emite:

  • SepararEstoque
  • NotificarCliente
  • GerarFatura

Cada handler consome o evento e produz o comando específico para o serviço que precisa agir.

Essa tradução não é um detalhe de implementação. É o mecanismo central que permite a EDA funcionar sem criar um emaranhado de dependências.

Sem o handler como adaptador, você tem duas alternativas ruins.

A primeira: o serviço que cria o pedido conhece e invoca diretamente todos os serviços que precisam reagir: estoque, notificação, faturamento. A pressão cognitiva sobre esse serviço cresce a cada novo consumidor.

A segunda: os serviços consumidores interpretam o evento diretamente como instrução de ação, acoplando sua lógica interna à estrutura de um evento que pertence a outro bounded context.

O handler elimina os dois problemas.

  • O produtor continua ignorando quem consome.
  • O consumidor recebe um comando limpo, com a interface que ele mesmo definiu.
  • O handler é a fronteira onde o acoplamento entre contextos é absorvido e isolado.

Cada seta entre o handler e o serviço destino é um comando.

Cada comando

  • tem contrato próprio
  • versionamento próprio
  • e pode ser invocado de qualquer origem

não apenas de um handler de evento.

Onde o reaproveitamento é legítimo e onde ele quebra?

Essa arquitetura tem três pontos com características de reaproveitamento completamente diferentes. Confundir esses três níveis é a origem da maioria dos problemas.

No consumo do evento, o reaproveitamento é o design.

Um único PedidoCriado pode ter dez handlers diferentes. Cada handler traduz o mesmo fato em um comando diferente para um serviço diferente.

Adicionar um novo comportamento ao sistema significa adicionar um novo handler:

  • sem alterar o produtor
  • sem alterar os handlers existentes

Open/closed principle aplicado na arquitetura, não apenas no código.

Na emissão do evento, o reaproveitamento é uma mentira.

O evento está amarrado ao fato que o originou. PedidoCriado só pode ser emitido quando um pedido é efetivamente criado.

Se você precisa disparar separação de estoque por outro motivo como:

  • uma correção manual
  • um reprocessamento
  • uma migração

emitir PedidoCriado é mentir para o sistema. O pedido não foi criado. Todos os handlers vão reagir como se tivesse sido. O cliente vai receber uma notificação de pedido que ele não fez. O faturamento vai gerar um documento fiscal duplicado. A semântica contamina todo o fluxo downstream.

Nos comandos, o reaproveitamento é monumental.

SepararEstoque pode ser invocado pelo handler de PedidoCriado.

  • Pode ser invocado por uma API de correção manual.
  • Pode ser invocado por um job de reprocessamento.
  • Pode ser invocado por um teste automatizado.

O comando não carrega contexto de origem, ele expressa uma capacidade do serviço. E capacidades são, por definição, reutilizáveis.

Quando alguém reclama que “precisou emitir o evento de novo porque era a única forma de disparar o fluxo”, o problema não é a arquitetura, o problema é que os comandos não foram desenhados como unidades independentes. O time construiu a integração inteira sobre eventos e não expôs os comandos como pontos de entrada autônomos.

O que acontece quando você emite eventos que não representam fatos?

Na teoria, a confusão entre evento e comando é um problema de modelagem. Na prática, as consequências são operacionais e caras.

Rastreabilidade falsa. Se PedidoCriado é emitido tanto pela criação real de um pedido quanto por uma rotina de correção, o log do sistema registra dois eventos semanticamente idênticos que representam situações completamente diferentes. Qualquer análise, auditoria ou debugging precisa de contexto externo para distinguir qual é qual. A observabilidade perde valor.

Efeitos colaterais incontroláveis. Cada handler que reage ao evento vai executar. Todos eles. Se você emitiu PedidoCriado só porque precisava do handler de estoque, vai ter que lidar com a notificação indevida, a fatura duplicada, e qualquer outro handler que o time tenha plugado desde a última vez que alguém olhou o fluxo completo. A quantidade de handlers cresce ao longo do tempo, e cada novo handler é um efeito colateral potencial para quem emite o evento fora de contexto.

Modelo mental corrompido. Quando o time percebe que eventos são emitidos sem que o fato correspondente tenha acontecido, a confiança no modelo se deteriora. Desenvolvedores começam a adicionar flags, campos de controle, condicionais dentro dos handlers para distinguir “evento real” de “evento forjado”. A complexidade acidental cresce até que ninguém confia na semântica de nenhum evento.

// Antipattern: forjando um evento para reaproveitar a cadeia
var eventoForjado = new PedidoCriado
{
    PedidoId = pedidoExistente.Id,
    // Campos preenchidos artificialmente
    // para disparar separação de estoque
};

await _bus.Publish(eventoForjado); 
// Todos os handlers vão reagir — inclusive os indesejados
// Correto: invocar o comando diretamente
var comando = new SepararEstoque
{
    PedidoId = pedidoExistente.Id,
    Itens = pedidoExistente.Itens,
    Origem = OrigemSeparacao.CorrecaoManual
};

await _bus.Send(comando); 
// Apenas o serviço de estoque reage

A diferença entre os dois trechos não é estilística. No primeiro, você acionou uma cadeia inteira de efeitos colaterais sem intenção. No segundo, você expressou exatamente o que precisava, para quem precisava.

A regra é simples, a disciplina é difícil

EventoComando
Um evento registra que algo aconteceuUm comando solicita que algo aconteça.
O handler de evento traduz fatos em solicitações.Os comandos são os pontos de entrada reutilizáveis dos seus serviços.

Quando você sente a necessidade de emitir um evento que não corresponde a um fato real, isso não é um sinal de que a arquitetura está limitada. É um sinal de que os comandos do serviço destino não estão expostos como deveriam.

O evento certo emitido pelo motivo errado causa mais dano do que o evento errado porque passa pelos validadores, é aceito pelos handlers, e contamina o sistema inteiro enquanto parece perfeitamente correto nos logs.

O Cloud Native .NET é meu principal projeto.

Onde empenho energia para ajudar, acompanhar, direcionar Desenvolvedores, Líderes Técnicos e jovens Arquitetos na jornada Cloud Native.

Conduzo entregando a maior e mais completa stack de tecnologias do mercado.

Ao trabalhar com desenvolvedores experientes, eu consigo usar seu aprendizado com .NET, banco de dados, e arquitetura para encurtar a jornada.

Ao restringir à desenvolvedores .NET eu consigo usar do contexto de tecnologias e problemas do seu dia-a-dia, coisas que você conhece hoje, como WCF, WebForms, IIS e MVC, por exemplo, para mostrar a comparação entre o que você conhece e o que está sendo apresentado.

É assim que construímos fundamentos sólidos, digerindo a complexidade com didática, tornando o complexo, simples.

É assim que conseguimos tornar uma jornada densa, em um pacote de ~4 meses.

Eu não acredito que um desenvolvedor possa entender uma tecnologia sem compreender seus fundamentos. Ele no máximo consegue ser produtivo, mas isso não faz desse desenvolvedor um bom tomador de decisões técnicas.

É preciso entender os fundamentos para conseguir tomar boas decisões.

0 comentários

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.


4x Microsoft MVP

Categorias

Assine

Luiz Carlos Faria

Mensagem do Autor

Espero que goste desse post. Não deixe de comentar e falar o que achou.

Se acha que esse post pode ajudar alguém que você conheça, compartilhe!

 

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.