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 carrinho
  • ItemRemovido: um item foi removido
  • QuantidadeAlterada: a quantidade de um item foi modificada
  • PedidoFinalizado: 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