Às vezes o evento parece exagerado.

Um pedido foi criado, mas o payload não traz só o orderId.

Ele traz itens, preços, moeda, endereço de entrega, forma de pagamento, status do cliente, canal de venda e alguns dados que, olhando rápido, parecem repetição de informação que já existe em outro serviço.

A primeira reação costuma ser:

"por que não mandar só o identificador?"

Parece mais limpo. Parece mais econômico. Parece menos redundante.

Até o consumidor precisar processar aquele evento sem depender de uma chamada síncrona para o serviço de pedidos, outra chamada para o serviço de clientes, outra para catálogo, outra para preços e talvez mais uma para logística.

É nesse ponto que o evento "grande demais" começa a revelar a intenção real do desenho.

Alguns eventos carregam mais dados porque estão transferindo estado junto com o fato.

Esse é o centro do Event-Carried State Transfer.


O que é Event-Carried State Transfer

Event-Carried State Transfer é um estilo de modelagem em que o evento carrega os dados relevantes do estado no momento em que o fato aconteceu.

Não é apenas uma notificação dizendo:

pedido 123 foi criado

É uma mensagem dizendo, com contexto suficiente:

pedido 123 foi criado com estes itens, estes valores, este cliente, este endereço e estas condições

A diferença parece pequena, mas muda bastante a arquitetura.

No primeiro caso, o consumidor recebe uma pista e precisa buscar o resto em outro lugar.

No segundo, o consumidor recebe uma representação suficiente do fato para seguir trabalhando com autonomia.

Isso não significa publicar a base inteira dentro do evento.

Significa carregar o estado necessário para que consumidores importantes consigam tomar decisões, montar projeções, registrar auditoria, atualizar seus próprios modelos ou disparar processos sem depender de consultas síncronas ao produtor.


Notificação não é transferência de estado

Existe uma diferença importante entre evento-notificação e evento com estado carregado.

Um evento-notificação diz que algo aconteceu.

Ele normalmente traz poucos dados:

  • identificador da entidade;
  • tipo do fato;
  • timestamp;
  • talvez um status novo.

Isso pode ser suficiente quando o consumidor só precisa saber que houve uma mudança e consegue consultar o produtor sem grandes riscos.

Mas em sistemas distribuídos, essa consulta extra costuma cobrar preço.

Ela cria dependência temporal entre serviços. O consumidor passa a precisar que o produtor esteja disponível naquele instante. Também cria uma leitura ambígua: o consumidor recebe um evento antigo, consulta a API agora e talvez encontre um estado mais novo do que o estado que existia quando o evento foi publicado.

O resultado é sutil.

O evento dizia uma coisa.

A consulta retornou outra.

E o consumidor processou uma mistura dos dois tempos.

Foi justamente por isso que, em outros posts, a série insistiu tanto em evento como contrato e significado de negócio. No post sobre evento não ser DTO, o ponto era não vazar implementação interna. Aqui, o ponto é complementar: carregar estado não é vazar DTO. É desenhar um contrato útil para integração.


Por que carregar mais dados pode reduzir acoplamento

Redundância assusta muita equipe.

Mas em arquitetura distribuída, eliminar toda redundância pode aumentar o acoplamento.

Imagine um consumer responsável por montar uma visão de pedidos para atendimento.

Se o evento traz apenas orderId, esse consumer precisa chamar o serviço de pedidos para detalhes, o serviço de clientes para nome e categoria, o serviço de catálogo para descrição dos produtos e talvez o serviço de pagamentos para condição de cobrança.

Agora o consumer não depende só do Kafka.

Ele depende da disponibilidade, latência, contrato HTTP, autenticação, versionamento e comportamento de vários serviços no momento do processamento.

Se uma dessas chamadas falha, o evento não consegue ser processado.

Se uma dessas APIs muda, o consumer quebra.

Se o estado consultado já mudou, a projeção pode nascer inconsistente.

Quando o evento carrega os dados relevantes, o consumidor fica mais autônomo. Ele não precisa montar o passado consultando o presente.

Ele recebe uma fotografia do fato no momento em que ele foi produzido.

Essa redundância é intencional.

Ela compra desacoplamento.


O evento precisa carregar o estado certo

O erro oposto também existe.

Algumas equipes entendem Event-Carried State Transfer como permissão para colocar qualquer coisa no payload.

Aí o evento vira um despejo de entidade, com campos internos, detalhes de persistência, flags temporárias, dados sensíveis e atributos que ninguém sabe se representam negócio ou conveniência local.

Isso volta ao problema discutido no post sobre DTO vs evento: evento não deveria ser espelho da implementação.

O payload precisa carregar o estado certo, não todo o estado.

Uma boa pergunta é:

"quais dados os consumidores precisam para processar este fato sem consultar o produtor imediatamente?"

Outra pergunta importante:

"esses dados fazem parte do significado público do evento ou são detalhe interno do serviço?"

No exemplo de um OrderCreated, pode fazer sentido carregar:

  • identificador do pedido;
  • identificador do cliente;
  • itens, quantidades e preços efetivos;
  • moeda;
  • endereço de entrega usado naquele pedido;
  • canal de origem;
  • regras comerciais aplicadas;
  • timestamp do fato;
  • versão do contrato.

Mas talvez não faça sentido carregar:

  • nomes de colunas internas;
  • flags de implementação;
  • dados sensíveis sem necessidade clara;
  • objetos aninhados que só existem porque a entidade Java estava assim;
  • campos que nenhum consumidor deveria conhecer.

O ponto não é evento pequeno contra evento grande.

O ponto é contrato útil contra vazamento acidental.


Estado carregado também melhora replay

Um dos valores fortes do Kafka é poder reler eventos.

Mas replay só é realmente útil quando o evento contém informação suficiente para reconstruir alguma coisa com coerência.

Se o tópico guarda apenas notificações com IDs, reprocessar o histórico pode virar uma sequência de chamadas ao estado atual dos serviços produtores.

Isso enfraquece a ideia de histórico.

Você não reconstrói o que aconteceu.

Você reconstrói uma leitura nova baseada no estado de agora.

Com eventos que carregam estado relevante, um consumidor consegue reconstruir projeções, alimentar relatórios, recompor read models e auditar decisões usando o contexto que existia na época do fato.

Isso não resolve todos os problemas de replay.

Ainda existem questões de schema, compatibilidade, retenção, correção de bugs e semântica de reprocessamento.

Mas sem dados suficientes no evento, o replay nasce limitado.

Ele depende demais de sistemas externos e de estados que talvez já não existam mais.


O risco de carregar dado demais

Event-Carried State Transfer não é grátis.

Payload maior aumenta custo de rede, armazenamento, serialização e processamento.

Também aumenta a responsabilidade sobre evolução de schema. Se mais campos viram contrato, mais campos precisam ser tratados com cuidado.

Além disso, existe risco real com dados sensíveis.

Um campo que parecia conveniente para um consumidor pode não dever circular por vários tópicos, ambientes, ferramentas de observabilidade e pipelines analíticos.

Outro risco é a obsolescência.

Se o evento carrega o endereço do cliente, ele está dizendo qual endereço foi usado naquele fato. Não está necessariamente dizendo qual é o endereço atual do cliente.

Essa diferença precisa estar clara no contrato.

Estado carregado geralmente é estado no tempo do evento.

Não é uma promessa de verdade atual.

É por isso que schema, versionamento e semântica explícita importam tanto. O post sobre schema em eventos tratou disso pelo lado formal: quando o payload vira contrato, sua evolução precisa ser controlada.


Quando usar esse estilo

Event-Carried State Transfer costuma fazer sentido quando consumidores precisam de autonomia.

Ele é especialmente útil quando:

  • consumidores montam projeções locais;
  • o fluxo precisa evitar chamadas síncronas em cadeia;
  • replay e auditoria importam;
  • a leitura do estado no momento do fato é mais importante do que a leitura do estado atual;
  • vários consumidores usam o mesmo contexto básico;
  • o produtor não deveria virar API obrigatória para todo processamento assíncrono.

Por outro lado, pode não valer a pena quando:

  • o dado é grande e raramente usado;
  • o consumidor realmente precisa sempre do estado mais atual;
  • o payload incluiria informação sensível sem necessidade;
  • o evento ficaria instável porque carrega detalhes demais;
  • o custo operacional do volume não compensa o desacoplamento.

Como quase tudo em arquitetura, a resposta madura não é "sempre carregue tudo" nem "mande só o ID".

A resposta é desenhar a fronteira.


Um jeito prático de decidir

Antes de publicar um evento, vale passar por algumas perguntas:

  • Um consumidor consegue processar o caso principal sem chamar o produtor?
  • O evento representa o estado no momento do fato ou depende de consulta posterior?
  • Os campos publicados são significado de negócio ou detalhe interno?
  • O replay desse tópico continuaria útil daqui a seis meses?
  • Existe dado sensível sendo distribuído sem necessidade?
  • A evolução desse payload está protegida por schema e compatibilidade?

Essas perguntas ajudam a separar intenção arquitetural de payload inchado.

Um evento carregado de estado não deveria parecer um acidente.

Ele deveria deixar claro por que aqueles dados estão ali.


O ponto que vale fixar

Alguns eventos carregam mais dados porque estão comprando autonomia para os consumidores.

Eles evitam chamadas síncronas, preservam contexto temporal, melhoram replay e reduzem acoplamento operacional entre serviços.

Mas isso só funciona quando o payload é desenhado como contrato.

Event-Carried State Transfer não é publicar DTO gigante no Kafka.

É carregar o estado necessário para que o fato de negócio continue compreensível fora do serviço que o produziu.

Evento pequeno demais pode parecer elegante.

Mas, se ele força todo consumidor a perguntar ao produtor o que realmente aconteceu, talvez ele não seja desacoplado.

Talvez ele só tenha terceirizado o acoplamento para a próxima chamada síncrona.