CQRS com Event Sourcing na prática
1. Fundamentos e Motivação para a Combinação
CQRS (Command Query Responsibility Segregation) e Event Sourcing formam uma dupla poderosa quando aplicados a domínios complexos. O CQRS separa operações de leitura e escrita em modelos distintos, enquanto o Event Sourcing persiste o estado como uma sequência imutável de eventos. Juntos, eles resolvem problemas que modelos CRUD tradicionais enfrentam em sistemas com alta concorrência, necessidade de auditoria completa e regras de negócio complexas.
Em um modelo CRUD tradicional, o estado atual é a única verdade. Isso gera conflitos em sistemas colaborativos, perde o histórico de alterações e dificulta a implementação de auditoria. Com Event Sourcing, cada mudança no estado é um evento imutável armazenado sequencialmente. O CQRS complementa essa abordagem ao permitir que o modelo de leitura seja otimizado para consultas, enquanto o modelo de escrita foca em validações e regras de negócio.
Cenários ideais para essa combinação incluem sistemas financeiros, e-commerce com carrinhos de compras colaborativos, plataformas de documentação com versionamento e qualquer sistema que exija trilha de auditoria completa.
2. Modelagem do Domínio com Eventos
A modelagem começa com a identificação dos eventos de domínio. Cada evento representa algo que aconteceu no sistema e que é relevante para o negócio. O agregado é a unidade de consistência transacional — um conjunto de entidades que devem ser atualizadas atomicamente.
Vamos modelar um sistema de carrinho de compras. Os eventos de domínio são:
ItemAdicionado: um item foi inserido no carrinhoItemRemovido: um item foi removidoQuantidadeAlterada: a quantidade de um item foi modificadaPedidoFinalizado: o carrinho foi convertido em pedido
O agregado Carrinho gerencia esses eventos. Sua fronteira inclui a lista de itens e o status do carrinho. Cada comando aplicado ao agregado gera eventos que são armazenados no stream do agregado.
Evento: ItemAdicionado
{
"id": "carrinho-123",
"itemId": "prod-456",
"quantidade": 2,
"precoUnitario": 29.90,
"timestamp": "2025-03-20T10:30:00Z",
"versao": 1
}
Evento: PedidoFinalizado
{
"id": "carrinho-123",
"total": 59.80,
"itens": 2,
"timestamp": "2025-03-20T10:35:00Z",
"versao": 5
}
3. Implementação do Lado de Comandos (Write Model)
O lado de comandos processa intenções de mudança. Um command handler recebe o comando, carrega o agregado a partir do stream de eventos, valida as regras de negócio e gera novos eventos.
O repositório de eventos armazena cada evento em um stream identificado pelo ID do agregado. A concorrência é tratada com controle de otimismo: cada evento possui um número de versão. Ao salvar, verifica-se se a versão do agregado carregado corresponde à versão atual no banco.
Command: AdicionarItem
{
"carrinhoId": "carrinho-123",
"itemId": "prod-456",
"quantidade": 2,
"precoUnitario": 29.90
}
Command Handler: AdicionarItemHandler
1. Carregar agregado Carrinho do stream "carrinho-123"
2. Validar: item já existe? Carrinho está ativo?
3. Gerar evento ItemAdicionado
4. Anexar evento ao stream com versão = versãoAtual + 1
5. Se versão conflitar, lançar exceção de concorrência
O repositório de eventos implementa duas operações principais:
RepositorioEventos:
- salvar(aggregateId, eventos[], versaoEsperada)
- carregarStream(aggregateId) -> eventos[]
4. Implementação do Lado de Consultas (Read Model)
As projeções transformam eventos em visões otimizadas para leitura. Cada projeção escuta eventos específicos e atualiza tabelas de consulta. A consistência é eventual — o modelo de leitura pode estar ligeiramente atrasado em relação ao modelo de escrita.
Para o sistema de carrinho, criamos duas projeções:
Projecao: PedidosPendentes
- Escuta: ItemAdicionado, ItemRemovido, PedidoFinalizado
- Mantém tabela: pedidos_pendentes
- carrinho_id, total, quantidade_itens, ultima_atualizacao
- Ao receber PedidoFinalizado: remove da tabela de pendentes
Projecao: HistoricoCliente
- Escuta: todos os eventos do carrinho
- Mantém tabela: historico_compras
- cliente_id, carrinho_id, evento_tipo, dados_evento, timestamp
A projeção assíncrona permite que o sistema de leitura seja escalado independentemente do sistema de escrita. Em cenários de alta demanda, múltiplas instâncias da projeção podem processar eventos em paralelo, desde que respeitem a ordenação por agregado.
5. Infraestrutura e Armazenamento de Eventos
A escolha do banco de eventos depende do volume e da complexidade. Bancos relacionais com tabela de eventos são uma opção inicial viável:
Tabela: eventos
- id (serial)
- aggregate_id (uuid)
- aggregate_type (varchar)
- event_type (varchar)
- data (jsonb)
- metadata (jsonb)
- versao (int)
- timestamp (timestamp)
- PRIMARY KEY (aggregate_id, versao)
Para sistemas mais maduros, bancos especializados como EventStoreDB oferecem funcionalidades como projeções embutidas, subscriptions e otimizações de performance.
O versionamento de eventos é crítico. À medida que o sistema evolui, os schemas dos eventos mudam. Estratégias comuns incluem:
- Upcasting: transformar eventos antigos no formato atual durante a leitura
- Versionamento por tipo: criar novas classes de evento (ex:
ItemAdicionadoV2) - Armazenamento flexível: usar JSONB e validar apenas campos obrigatórios
Snapshots otimizam a reconstrução de agregados longos. Periodicamente, salva-se o estado atual do agregado em um snapshot. Ao carregar, começa-se do snapshot mais recente e aplicam-se apenas os eventos posteriores.
Tabela: snapshots
- aggregate_id (uuid)
- versao (int)
- estado (jsonb)
- timestamp (timestamp)
- PRIMARY KEY (aggregate_id, versao)
6. Tratamento de Erros, Consistência e Resiliência
Falhas em projeções são inevitáveis. A idempotência garante que reprocessar o mesmo evento não cause duplicação. Cada evento possui um identificador único; a projeção registra quais eventos já foram processados.
Projecao com checkpoint:
- checkpoint_id: ultimo_evento_id processado
- Ao falhar: reiniciar do checkpoint
- Ao processar: verificar se evento já foi aplicado
Em sistemas distribuídos, a ordenação de eventos é garantida pelo stream do agregado. Cada agregado possui sua sequência ordenada. Para projeções que cruzam agregados, a ordenação global não é garantida — a projeção deve ser tolerante a isso.
Transações distribuídas são evitadas. Em vez delas, utilizam-se sagas: sequências de ações locais com compensações para desfazer operações em caso de falha.
Saga: Finalizar Pedido
1. Comando: FinalizarPedido (Write Model)
2. Evento: PedidoFinalizado
3. Projeção: AtualizarEstoque (Read Model)
4. Se falhar: Evento: EstoqueInsuficiente
5. Compensação: Evento: PedidoCancelado
7. Quando Evitar e Armadilhas Comuns
CQRS com Event Sourcing adiciona complexidade operacional significativa. É overengineering para sistemas CRUD simples, onde um banco relacional com histórico em tabelas de auditoria seria suficiente.
Armadilhas comuns incluem:
- Projeções frágeis: mudanças no schema de eventos quebram projeções existentes
- Custos de armazenagem: cada evento é imutável e permanente; volumes massivos exigem estratégias de retenção
- Performance de reconstrução: agregados com milhares de eventos tornam a reconstrução lenta sem snapshots adequados
- Monitoramento complexo: projeções atrasadas ou paradas exigem alertas e dashboards específicos
A complexidade operacional inclui gerenciamento de múltiplos bancos de dados, filas de eventos, subscriptions e reprocessamento de projeções. Times pequenos ou sistemas com domínios simples devem considerar alternativas mais leves.
Referências
- Microsoft - CQRS Pattern — Documentação oficial da Microsoft sobre o padrão CQRS, com exemplos e considerações de implementação
- Event Store Documentation — Documentação oficial do EventStoreDB, banco de eventos especializado para Event Sourcing
- Martin Fowler - CQRS — Artigo de Martin Fowler explicando os fundamentos do CQRS e quando aplicá-lo
- Greg Young - Event Sourcing — Documento seminal de Greg Young sobre CQRS e Event Sourcing, abordando conceitos avançados
- Udi Dahan - Clarified CQRS — Artigo técnico de Udi Dahan que esclarece mal-entendidos comuns sobre CQRS e sua implementação prática