CQRS: separando leituras e escritas

1. Introdução ao CQRS

O padrão CQRS (Command Query Responsibility Segregation) foi formalmente introduzido por Greg Young e popularizado por Martin Fowler. Sua premissa central é simples, mas poderosa: separar os modelos e operações de leitura (queries) dos de escrita (commands). Em sistemas tradicionais, um mesmo modelo de domínio é usado tanto para ler quanto para modificar dados, o que frequentemente gera compromissos indesejados.

A diferença fundamental reside na natureza dos comandos e consultas. Comandos representam intenções de mudança de estado — eles devem ser validados, podem gerar efeitos colaterais e não retornam dados de domínio. Consultas, por outro lado, recuperam dados sem alterar o estado do sistema. Ao segregar essas responsabilidades, o CQRS resolve problemas como modelos de domínio excessivamente complexos (que tentam servir a múltiplos propósitos) e gargalos de escalabilidade onde leituras e escritas competem pelos mesmos recursos.

2. Fundamentos: Comandos e Consultas

Comandos são objetos que encapsulam uma intenção de alterar o estado do sistema. Eles são nomeados no imperativo (ex: CriarPedido, AtualizarEstoque) e devem conter apenas os dados necessários para a operação. Um comando não retorna dados de domínio — no máximo, retorna um identificador ou confirmação de sucesso.

Exemplo de comando:

Command: CriarPedido
{
  clienteId: "123",
  itens: [
    { produtoId: "456", quantidade: 2 }
  ],
  enderecoEntrega: "Rua A, 100"
}

Consultas são operações que retornam dados sem modificar o estado. Elas são otimizadas para a forma como os dados serão exibidos ou processados. Uma consulta pode retornar DTOs (Data Transfer Objects) específicos para cada caso de uso.

Exemplo de consulta:

Query: ObterResumoPedido
{
  pedidoId: "789"
}
// Retorna: { status, valorTotal, itensResumidos }

A separação de modelos significa que o modelo de escrita é rico em regras de negócio e validações, enquanto o modelo de leitura é desnormalizado e otimizado para consultas rápidas.

3. Arquitetura e Componentes do CQRS

A arquitetura CQRS introduz duas camadas principais:

Camada de Comando:
- Command Bus: roteia comandos para seus respectivos handlers
- Command Handlers: contêm a lógica de validação e aplicação das regras de negócio
- Validação: garante que o comando atenda aos pré-requisitos antes da execução

Camada de Consulta:
- Query Handlers: executam consultas otimizadas no banco de leitura
- Projeções: dados desnormalizados prontos para consumo
- DTOs: estruturas de dados específicas para cada tela ou relatório

O armazenamento é geralmente separado:
- Banco de escrita: normalizado, otimizado para integridade e operações transacionais
- Banco de leitura: desnormalizado, com tabelas planas e índices otimizados para consultas

4. Sincronização entre Modelos de Leitura e Escrita

A sincronização entre os modelos é feita através de projeções. Existem duas estratégias principais:

Projeção síncrona: após o comando ser processado, o banco de leitura é atualizado imediatamente na mesma transação. Isso garante consistência forte, mas pode impactar a performance de escrita.

Projeção assíncrona: o comando é processado e um evento de domínio é publicado. Um subscriber processa esse evento e atualiza o banco de leitura de forma independente. Isso introduz consistência eventual, mas oferece melhor desempenho e resiliência.

Exemplo de fluxo assíncrono:

1. Comando "CriarPedido" chega ao Command Bus
2. Command Handler valida e persiste no banco de escrita
3. Handler publica evento "PedidoCriado"
4. Evento é processado por um Projector
5. Projector atualiza a projeção no banco de leitura
6. Cliente pode consultar os dados eventualmente consistentes

A consistência eventual exige que o sistema tolere atrasos entre a escrita e a leitura, o que é aceitável em muitos cenários de negócio.

5. Benefícios e Desafios do CQRS

Benefícios:
- Escalabilidade independente: é possível escalar os serviços de leitura e escrita separadamente
- Otimização de desempenho: cada modelo é ajustado para seu propósito específico
- Equipes paralelas: times diferentes podem trabalhar nos modelos de leitura e escrita
- Segurança: é possível aplicar políticas de acesso distintas para leituras e escritas

Desafios:
- Complexidade adicional: a arquitetura exige mais componentes e coordenação
- Duplicação de dados: os mesmos dados podem existir em múltiplos formatos e locais
- Gerenciamento de consistência: a consistência eventual requer mecanismos de reconciliação
- Maior custo operacional: mais bancos de dados e serviços para gerenciar

Quando aplicar CQRS:
- Sistemas com alta carga de leitura assimétrica em relação à escrita
- Domínios complexos onde o modelo de escrita precisa ser rico e o de leitura simples
- Cenários onde diferentes representações dos mesmos dados são necessárias para diferentes casos de uso

6. CQRS sem Event Sourcing (CQRS Puro)

É possível implementar CQRS sem Event Sourcing, utilizando bancos de dados relacionais tradicionais. Nessa abordagem, o banco de escrita armazena o estado atual das entidades, e o banco de leitura armazena projeções desnormalizadas.

Exemplo de fluxo completo (CQRS puro):

// Escrita
1. Cliente envia comando "AtualizarEmailUsuario"
   { usuarioId: 1, novoEmail: "novo@email.com" }

2. Command Handler valida regras:
   - Novo email não está em uso
   - Formato de email é válido

3. Handler persiste no banco de escrita:
   UPDATE usuarios SET email = 'novo@email.com' WHERE id = 1;

4. Handler publica evento "EmailUsuarioAtualizado"
   { usuarioId: 1, emailAnterior: "antigo@email.com", novoEmail: "novo@email.com" }

// Leitura (assíncrona)
5. Projector recebe o evento e atualiza a projeção:
   UPDATE proj_usuarios_resumo SET email = 'novo@email.com' WHERE usuarioId = 1;

6. Consulta "ObterResumoUsuario" agora retorna os dados atualizados

A diferença para Event Sourcing é que o banco de escrita não armazena o histórico completo de eventos, apenas o estado atual. Isso simplifica a implementação, mas sacrifica a capacidade de reconstruir estados passados.

7. Padrões Relacionados e Considerações Finais

O CQRS se relaciona fortemente com Domain-Driven Design (DDD) e seus bounded contexts. Cada contexto delimitado pode ter seu próprio modelo de leitura e escrita, respeitando a linguagem ubíqua do domínio.

Comparado ao CRUD tradicional, o CQRS oferece mais flexibilidade para cenários complexos, mas introduz complexidade desnecessária em sistemas simples. Já o Event Sourcing complementa o CQRS naturalmente, mas não é um requisito obrigatório.

Boas práticas:
- Versionamento de projeções: evolua as projeções de leitura de forma independente, permitindo migrações graduais
- Monitoramento de consistência: implemente métricas para detectar atrasos na sincronização entre modelos
- Comece simples: evite CQRS em sistemas com domínios triviais ou baixa carga assimétrica

O CQRS é uma ferramenta poderosa quando aplicada nos contextos corretos. Sua adoção deve ser motivada por necessidades reais de escalabilidade, complexidade de domínio ou requisitos de desempenho, e não por modismo arquitetural.

Referências