CQRS na prática: separando leitura e escrita no seu sistema
1. O que é CQRS e por que separar comandos de consultas?
CQRS (Command Query Responsibility Segregation) é um padrão arquitetural que propõe a separação explícita entre operações que modificam o estado do sistema (comandos) e operações que apenas consultam dados (consultas). Diferentemente do CRUD tradicional, onde uma única entidade serve tanto para leitura quanto para escrita, o CQRS permite que cada lado evolua de forma independente, com modelos de dados otimizados para sua finalidade específica.
A separação faz sentido quando seu sistema apresenta assimetria entre operações de leitura e escrita — por exemplo, um sistema de e-commerce onde milhares de usuários consultam catálogos simultaneamente, mas poucos realizam pedidos. Em sistemas simples com volume balanceado, o CQRS pode adicionar complexidade desnecessária.
2. Arquitetura básica de um sistema CQRS
Os componentes fundamentais de uma arquitetura CQRS incluem:
- Command Handlers: processam comandos validando regras de negócio e persistindo alterações
- Query Handlers: executam consultas otimizadas contra modelos de leitura
- Event Bus: canal de comunicação para propagar mudanças do modelo de escrita para o de leitura
- Bancos separados: banco de escrita (normalizado, focado em consistência) e banco de leitura (desnormalizado, focado em performance)
O fluxo típico de dados segue: comando → validação → persistência no banco de escrita → publicação de evento → atualização do banco de leitura → consulta eventualmente consistente.
3. Implementando o lado de escrita (Commands)
Um comando representa uma intenção clara de modificar o sistema. Diferente de uma requisição CRUD, ele carrega semântica de negócio:
// Exemplo de estrutura de comando
Command: CriarPedido
{
clienteId: "12345",
itens: [
{ produtoId: "PROD-001", quantidade: 2 },
{ produtoId: "PROD-002", quantidade: 1 }
],
enderecoEntrega: "Rua das Flores, 100"
}
O Command Handler valida regras como estoque disponível, limite de crédito do cliente e consistência dos dados antes de persistir. O banco de escrita mantém tabelas normalizadas com integridade referencial forte, otimizadas para operações transacionais.
// Pseudocódigo de um Command Handler
CommandHandler: CriarPedidoHandler
1. Validar cliente existe e tem crédito
2. Validar estoque para cada item
3. Calcular valores e impostos
4. Persistir pedido (tabela pedidos + itens_pedido)
5. Publicar evento "PedidoCriado"
4. Implementando o lado de leitura (Queries)
O modelo de leitura é desnormalizado e otimizado para consultas específicas. Enquanto o banco de escrita pode ter dezenas de tabelas relacionadas, o banco de leitura pode ter uma única tabela agregando todas as informações necessárias para uma tela:
// Modelo de leitura desnormalizado para dashboard de pedidos
Tabela: pedidos_dashboard
{
pedidoId: "PED-202401-001",
clienteNome: "João Silva",
dataPedido: "2024-01-15",
valorTotal: 450.00,
status: "EM_TRANSITO",
quantidadeItens: 3,
prazoEntrega: "2024-01-20"
}
Os Query Handlers executam consultas diretas e eficientes, aproveitando índices específicos e cache. Não há joins complexos ou regras de negócio — apenas recuperação de dados prontos para exibição.
5. Sincronização entre os modelos: Eventual Consistency
A propagação de mudanças do modelo de escrita para o de leitura é feita através de eventos assíncronos. Os mecanismos comuns incluem:
- Eventos publicados em fila (RabbitMQ, Kafka): cada evento carrega dados suficientes para atualizar projeções
- Polling: o banco de leitura verifica periodicamente por mudanças no banco de escrita
- Change Data Capture (CDC): monitora o log de transações do banco de escrita
A consistência eventual significa que o modelo de leitura pode estar ligeiramente desatualizado em relação ao de escrita. Para a maioria dos casos de negócio, uma latência de segundos é aceitável. Estratégias como "stale reads" ou indicadores visuais ("dados atualizados há 2 segundos") ajudam a gerenciar expectativas do usuário.
6. Exemplo prático passo a passo (sem código)
Cenário: Sistema de pedidos de uma loja online
-
Comando "CriarPedido": O cliente finaliza a compra. O sistema valida estoque, calcula frete e persiste o pedido no banco de escrita (tabelas normalizadas: pedidos, itens_pedido, pagamentos).
-
Evento publicado: "PedidoCriado" contendo dados como ID do pedido, itens, valor total e endereço.
-
Projeção atualizada: Um worker consome o evento e atualiza múltiplas projeções de leitura:
- Tabela
pedidos_cliente(para o histórico do cliente) - Tabela
relatorio_vendas_diario(para dashboard financeiro) -
Cache Redis com os últimos 100 pedidos
-
Consulta de relatório: O gerente acessa o dashboard de vendas. O Query Handler consulta diretamente a tabela
relatorio_vendas_diario, que já possui dados agregados por dia, sem precisar calcular em tempo real. -
Consulta de pedido individual: O cliente visualiza o status do pedido. A consulta busca na projeção
pedidos_cliente, que inclui dados desnormalizados como nome do produto e status de entrega.
7. Boas práticas e armadilhas comuns
Boas práticas:
- Comece separando apenas os modelos de dados, mantendo a mesma base de código
- Use event sourcing apenas quando precisar de auditoria completa ou reconstrução de estado
- Implemente versionamento de projeções para evoluir modelos de leitura sem quebrar consultas existentes
- Monitore a latência entre escrita e leitura com métricas claras
Armadilhas:
- Aplicar CQRS em sistemas CRUD simples adiciona complexidade sem benefício
- Ignorar a consistência eventual pode causar bugs difíceis de rastrear
- Manter múltiplos bancos aumenta custos operacionais e complexidade de deploy
- Transações distribuídas entre bancos de leitura e escrita devem ser evitadas — prefira compensações manuais
8. Integração com outros padrões (DDD, Event Sourcing, Caching)
CQRS + DDD: Os agregados do Domain-Driven Design se tornam os guardiões da consistência no lado de escrita. Comandos representam intenções de negócio (ex: "AdicionarItemAoCarrinho"), enquanto os agregados validam regras antes de persistir.
CQRS + Event Sourcing: Em vez de armazenar o estado atual, cada comando gera eventos que são armazenados como fonte da verdade. As projeções de leitura são construídas reproduzindo esses eventos. Isso oferece auditoria completa e capacidade de reconstruir estados passados, mas exige gerenciamento cuidadoso de versões de eventos.
CQRS + Cache distribuído: Projeções de leitura podem ser armazenadas em Redis ou Memcached para consultas de altíssima performance. O cache é invalidado ou atualizado quando eventos de mudança são processados, garantindo que dados quentes estejam sempre disponíveis com latência de microssegundos.
Referências
- Microsoft - CQRS pattern — Documentação oficial da Microsoft sobre o padrão CQRS, com diagramas de arquitetura e considerações de implementação
- Martin Fowler - CQRS — Artigo clássico de Martin Fowler explicando os fundamentos e quando aplicar o padrão
- Greg Young - CQRS Documents — Documento seminal de Greg Young, criador do termo CQRS, com detalhes técnicos aprofundados
- AWS - Implementing CQRS with Event Sourcing — Guia prático da AWS sobre implementação de CQRS com event sourcing em infraestrutura cloud
- Udi Dahan - Clarified CQRS — Artigo técnico de Udi Dahan esclarecendo conceitos e mitos comuns sobre CQRS na prática
- Khalil Stemmler - CQRS and Event Sourcing in TypeScript — Tutorial prático com exemplos de código em TypeScript implementando CQRS do zero