05 - CQRS com Event Sourcing (Combinação)
video: https://youtu.be/... no frontmatter da nota.CQRS com Event Sourcing: A Combinação
A combinação de CQRS com Event Sourcing forma o arranjo mais completo da família orientada a eventos: o log de eventos é a fonte de verdade do lado de escrita, e as projeções materializadas a partir dele são os modelos de leitura. O Event Sourcing dá ao CQRS uma sincronização com semântica de negócio, porque os eventos que alimentam as visões são fatos do domínio, e não linhas alteradas. O CQRS dá ao Event Sourcing a resposta para a pergunta mais cara do padrão, que é consultar estado atual sem reidratar streams inteiros. Os dois padrões não se exigem mutuamente, mas juntos resolvem as principais fraquezas um do outro, ao preço de acumular a complexidade operacional dos dois. Por isso a decisão de combinar deve passar por critérios objetivos de carga, auditoria e evolução, e não por preferência estética. Entender o papel exato de cada peça é o que permite adotar a combinação sem transformar o sistema em um exercício de arquitetura.
Como os dois padrões se encaixam
No lado de escrita, o Event Sourcing define a mecânica: o comando carrega o agregado por replay do stream, valida as regras, gera novos eventos e os anexa ao log com verificação de versão esperada. Nenhum estado corrente é sobrescrito; a mudança é sempre um acréscimo de fato. O agregado, o repositório e o event store formam o núcleo de decisão, e a única saída desse núcleo é o próprio log.
No lado de leitura, o CQRS define a forma: cada consulta relevante recebe uma projeção dedicada, desenhada para a tela ou para o relatório que a consome. As projeções assinam o fluxo global de eventos, aplicam transformações e gravam os resultados em bases otimizadas para leitura, com checkpoints próprios. A direção é única e irreversível: comando gera evento, evento alimenta projeção, projeção serve consulta. Nenhuma leitura escreve de volta, e nenhuma visão é fonte de verdade.
O problema que a combinação resolve
CQRS sozinho deixa aberta a pergunta de como o read model fica sabendo das mudanças. As respostas comuns são gatilhos, dupla gravação no código da aplicação ou captura de dados alterados, e todas têm limites conhecidos: o gatilho replica linhas sem semântica, a dupla gravação corre risco de inconsistência entre duas escritas, e a captura de mudanças reflete tabelas, não fatos. Quando a visão precisa de uma regra de negócio para ser construída, como somar o total de um pedido a partir dos itens, qualquer uma dessas opções exige artifício.
Event Sourcing sozinho deixa aberta a pergunta oposta: como consultar. A leitura de estado exige reidratar o agregado, e leituras de lista, busca ou relatório não têm de onde vir sem varreduras caras de todos os streams. A combinação fecha as duas pontas ao mesmo tempo: os eventos já existem por construção, com semântica completa, e as projeções os transformam em consultas baratas. O custo dessa elegância é pagar a infraestrutura dos dois padrões de uma vez, e é exatamente por isso que a adoção precisa de justificativa medida.
Funcionamento interno: do comando à projeção
O caminho completo de uma mudança passa por etapas bem definidas.
- O comando chega ao handler com a identidade do agregado e os dados da intenção.
- O agregado é reidratado por replay do stream, ou a partir de snapshot, e a versão atual fica conhecida.
- As regras de negócio são executadas e geram eventos pendentes, ou recusam a operação.
- Os eventos são anexados ao store com a versão esperada; conflito de concorrência força nova execução.
- O projetor recebe o evento pela assinatura do fluxo global, aplica a transformação da visão e avança o checkpoint.
- A consulta passa a refletir a mudança, com a defasagem medida e tratada como consistência eventual.
// Lado de escrita: comando carrega o agregado do stream e anexa eventos
public class FecharPedidoHandler {
public void executar(FecharPedido comando) {
Pedido pedido = repositorio.carregar(comando.pedidoId()); // replay do stream
pedido.fechar(); // regras e invariantes
eventStore.acrescentar(
"pedido-" + comando.pedidoId(),
pedido.versaoEsperada(), // concorrência otimista por stream
pedido.eventosPendentes()); // fatos gerados, e não linhas atualizadas
// nada de tocar na visão aqui: a projeção cuida disso, de forma assíncrona
}
}
// Lado de leitura: projetor consome o fluxo global e mantém a visão
public class ResumoPedidoProjecao {
public void processar(EventoEnvolto envolto) {
if (envolto.tipo().equals("PedidoPago")) {
resumoDao.atualizarStatus(envolto.pedidoId(), "PAGO"); // formato da tela
}
if (envolto.tipo().equals("PedidoCancelado")) {
resumoDao.atualizarStatus(envolto.pedidoId(), "CANCELADO");
}
checkpoint.gravar(envolto.posicaoGlobal()); // idempotência por posição
}
}
A sincronização entre os lados herda as regras de consumo de log: entrega at-least-once, idempotência no projetor, ordenação garantida por stream e checkpoints duráveis. A diferença para um CQRS sem Event Sourcing é que a fila de sincronização é o próprio log de eventos, com ordem e significado, e não um canal técnico adicional para operar. Menos infraestrutura no caminho, mais disciplina no consumo.
Graus de adoção da combinação
Assim como o CQRS isolado, a combinação não é um interruptor liga-desliga, é uma rampa de degraus com custo crescente. O erro frequente é começar no degrau mais alto por inspiração de artigo, e o caminho saudável é subir quando a medida de latência, carga ou variedade de consulta justificar.
| Degrau | Descrição | Custo adicional | Quando usar |
|---|---|---|---|
| ES sem projeções | Leituras apenas por reidratação do agregado | Nenhum além do próprio ES | Pouca consulta, foco em histórico e integração |
| Projeções na mesma base | Uma ou duas visões em tabelas derivadas do mesmo banco | Baixo; projetor único e transação comum | Listagens sobre streams longos, começo da separação |
| Múltiplas visões físicas | Read models em tecnologias distintas, sincronizados por eventos | Médio; pipeline completo e consistência eventual | Escala de leitura e formas de consulta variadas |
| Serviços separados | Escrita e leitura em serviços independentes, escalados à parte | Alto; operação distribuída completa | Times distintos, escalas distintas, domínio grande |
A maioria dos sistemas que precisa da combinação vive nos degraus 2 e 3. O degrau 4 é raro e deve ser precedido de evidência de que a operação distribuída é suportada pelo time, porque o custo de deploys, versionamento e observabilidade duplica nele.
Critérios objetivos de adoção
A combinação cobra caro, e a decisão merece critérios verificáveis, e não entusiasmo.
- Auditoria e conformidade exigem histórico completo por construção; se o negócio já precisaria manter tabela de log paralela, o Event Sourcing paga a si mesmo.
- A razão entre leituras e escritas é alta e medida, com formas de consulta variadas que justificam múltiplas visões do mesmo fato.
- Há necessidade real de reconstruir modelos, seja para migração, seja para responder perguntas novas sobre o passado; o replay é o argumento exclusivo do Event Sourcing.
- O time tem maturidade em idempotência, versionamento de eventos e observabilidade de defasagem; sem isso, o custo de operação supera o ganho.
- Consultas e escritas têm perfis de escala incompatíveis em um único modelo, com picos comprovados e não hipotéticos.
Se apenas um critério se aplica, adote só o padrão que o atende. Auditoria sem assimetria de leitura pede Event Sourcing com leituras simples; assimetria de leitura sem exigência de histórico pede CQRS sobre banco convencional, possivelmente com captura de mudanças. A combinação é a resposta quando os dois conjuntos de critérios aparecem juntos no mesmo contexto de domínio.
Quando cada padrão funciona sozinho
CQRS sem Event Sourcing atende bem sistemas cujo problema é a forma da consulta: relatórios que não podem pesar na base transacional, buscas textuais, telas com desnormalização agressiva. A escrita continua relacional, as invariantes ficam protegidas por transações, e as visões nascem de sincronização na mesma transação ou por captura de dados alterados. É o ponto de partida da maioria dos times, e muitos nunca precisam sair dele.
Event Sourcing sem CQRS estruturado é menos comum, mas existe: entidades de baixa consulta, como configurações, contratos e registros fiscais, cujo valor está no histórico e na integração por eventos. A leitura de estado acontece por reidratação no caminho de comando, e as poucas consultas externas consomem os eventos publicados. Nesse arranjo não há um lado de leitura estruturado, e o custo de projeções não é pago. A fronteira entre isso e a combinação é justamente o volume e a variedade de consultas.
Sinais de que o Event Sourcing basta sozinho:
- A entidade recebe centenas de comandos por dia, mas as consultas de estado são poucas dezenas.
- O consumidor externo principal é outro sistema, que assina os eventos e mantém a própria visão.
- O histórico é o produto: auditoria, rastreabilidade e reprodução de decisões passadas.
Sinais de que o CQRS basta sozinho:
- A assimetria é de consulta, e o histórico detalhado não tem consumidor regulatório nem analítico.
- As visões podem ser derivadas de tabelas com transformação trivial, sem regra de negócio na montagem.
- O time não tem maturidade para idempotência, versionamento de eventos e replay, e o prazo não permite construí-la.
Comparação entre os três arranjos
| Aspecto | CQRS sem ES | ES sem CQRS estruturado | ES + CQRS |
|---|---|---|---|
| Fonte de verdade | Tabelas normalizadas | Log de eventos | Log de eventos |
| Sincronização da leitura | Transação, gatilho ou CDC | Não se aplica; leitura por replay | Assinatura de eventos com checkpoint |
| Consulta de estado atual | Direta e imediata | Cara, exige reidratação | Projeção pronta, eventual |
| Auditoria histórica | Parcial, depende de log extra | Completa por construção | Completa por construção |
| Reconstrução de visões | Limitada ao dado corrente | Possível por replay total | Possível por replay total |
| Complexidade operacional | Baixa a média | Média | Alta |
| Cenário típico | Relatórios sobre CRUD estável | Entidades históricas de pouca consulta | Alta escala de leitura com histórico rico |
A complexidade acumulada dos dois padrões
Combinar é multiplicar superfícies de manutenção, e o time precisa saber o que entra na conta. Do lado do Event Sourcing: versionamento de eventos com upcasting, snapshots, replay em produção, backup do store e política de retenção. Do lado do CQRS: um read model por consulta, defasagem visível para o usuário, contratos de leitura versionados e sincronização monitorada. Juntos, os dois exigem idempotência em toda parte, correlação ponta a ponta e testes que cobrem o caminho completo do comando até a visão.
A acumulação também aparece nas equipes e nos processos. Toda mudança de modelo passa por duas perguntas: quais eventos mudam e quais visões reagem. Cada evento novo é um contrato público; cada visão nova é um consumidor permanente, com checkpoint e operação próprios. Times pequenos tendem a subestimar esse custo acumulado, e a saída madura é começar por um único contexto de domínio com alta assimetria, e não pela arquitetura inteira de uma vez.
O checklist operacional mínimo da combinação resume o que precisa estar de pé antes do primeiro cliente real:
- Assinatura do fluxo global com reconexão automática e checkpoint persistido por projeção.
- Idempotência de projetor verificada por teste, usando reentrega de evento como cenário padrão.
- Métrica de lag por projeção e alerta de crescimento sustentado, com runbook de reprocessamento.
- Teste de reconstrução completa das visões a partir do log, executado em ambiente isolado com dados mascarados.
- Política de backup e restauração do store, com ensaio de restauração periódico e documentado.
- Contratos de leitura versionados, porque o DTO publicado para outras telas e sistemas é interface pública.
Erros comuns e boas práticas
- Combinar os padrões por padrão em sistemas novos; comece pelo degrau que resolve o problema medido, e suba quando a evidência aparecer.
- Escrever na visão dentro do handler de comando; a sincronização é papel da projeção, e esse atalho reintroduz a dupla gravação disfarçada.
- Criar projeções para consultas raras; consultas frias podem ler o store sob demanda, sem visão permanente para manter.
- Ignorar a defasagem no contrato da API de leitura; a combinação herda toda a consistência eventual do CQRS, com os mesmos padrões de mitigação.
- Tratar o replay como alternativa à projeção para consultas quentes; replay é ferramenta de reconstrução, e não caminho de consulta.
- Esquecer de testar a reconstrução completa das visões; o teste de replay ponta a ponta é o seguro da arquitetura.
- Deixar checkpoints sem observabilidade; a posição global processada por projeção precisa aparecer em painel e alerta.
- Acoplar o formato do evento ao formato da tela; o evento registra o fato, e a projeção decide a forma da exibição.
- Subir todos os contextos para a combinação de uma vez; pilote em um domínio, extraia as ferramentas comuns, e depois expanda.
- Modelar eventos como espelho do banco relacional antigo; desenhe fatos do domínio, e não clones de tabela.
Os sintomas mais frequentes quando a combinação está mal adotada seguem padrão reconhecível.
| Sintoma em produção | Causa provável | Mitigação |
|---|---|---|
| Visões defasadas por minutos em picos | Projetor pesado competindo com reprocessamento | Priorizar eventos, limitar concorrência de replay e medir lag por etapa |
| Dados duplicados no read model | Projetor sem idempotência sob reentrega | Deduplicar por posição global e testar reentrega em CI |
| Time evita mudar o modelo por medo do impacto | Acoplamento implícito entre evento e telas | Separar fato de exibição na projeção e versionar contratos |
| Incidente de store sem caminho de recuperação | Backup sem ensaio de restauração | Rotina periódica de restauração com validação de replay |
Práticas que consolidam a combinação: documente, para cada visão, os eventos que a alimentam e a posição do checkpoint; automatize o teste de reconstrução das visões com dados reais mascarados; meça a defasagem ponta a ponta como o número principal do sistema; e revise periodicamente se cada projeção ainda justifica sua existência, desativando as que perderam consumidor. A combinação entrega o melhor dos dois padrões quando cada peça cumpre um papel claro: o log guarda o que aconteceu, as projeções respondem como está, e os comandos decidem o que pode acontecer.