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

  1. 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).

  2. Evento publicado: "PedidoCriado" contendo dados como ID do pedido, itens, valor total e endereço.

  3. Projeção atualizada: Um worker consome o evento e atualiza múltiplas projeções de leitura:

  4. Tabela pedidos_cliente (para o histórico do cliente)
  5. Tabela relatorio_vendas_diario (para dashboard financeiro)
  6. Cache Redis com os últimos 100 pedidos

  7. 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.

  8. 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