CQRS
Segrega as operações de escrita (Commands — alteram estado, não retornam dados) das de leitura (Queries — não alteram estado, retornam dados), permitindo que cada lado evolua, escale e seja otimizado de forma independente.
Intenção
Separar o modelo que muda o estado do sistema do modelo que lê esse estado. CQRS (Command Query Responsibility Segregation) é a aplicação arquitetural do princípio CQS de Bertrand Meyer — que proíbe um mesmo método de fazer as duas coisas ao mesmo tempo — elevando essa separação ao nível de modelos distintos, handlers distintos e, nos graus mais avançados, bancos de dados distintos.
Um Command expressa uma intenção de mudança: CriarPedido, CancelarReserva, AtualizarEstoque. Ele é processado por um CommandHandler que valida regras de negócio, muta o estado e retorna apenas uma confirmação ou nada. Uma Query expressa uma pergunta: ListarPedidosPorCliente, ObterSaldoConta. Ela é respondida por um QueryHandler que lê os dados e os retorna sem jamais alterar o estado.
Essa separação resolve um atrito fundamental: o modelo ideal para escrever dados (com invariantes de domínio protegidas, agregados com consistência transacional, validação rica) costuma ser um modelo ruim para ler dados (que frequentemente exige joins de múltiplas entidades, projeções específicas por caso de uso e formatos otimizados para exibição). Com CQRS, cada lado pode ser modelado da forma mais adequada para sua função.
Problema
Em sistemas com um único modelo compartilhado de leitura e escrita, surgem conflitos que crescem com o tempo:
- Impedância de leitura vs. escrita: o modelo de domínio com agregados encapsulados e invariantes protegidas é excelente para garantir consistência na escrita, mas péssimo para consultas que precisam de dados desnormalizados, calculados ou projetados de formas variadas. O resultado são queries complexas com joins profundos sobre um modelo que não foi projetado para ser consultado.
- Escalabilidade assimétrica: a maioria dos sistemas tem carga de leitura muito maior que carga de escrita. Escalar um único modelo para os dois casos força um compromisso subótimo: ou o modelo de escrita é sacrificado para otimizar leituras, ou as leituras ficam lentas para preservar as garantias de escrita.
- Queries que expõem detalhes internos do domínio: para atender relatórios e dashboards, o domínio é frequentemente obrigado a expor estruturas internas que não fazem parte de nenhum caso de uso de negócio — violando o encapsulamento do agregado.
- Contention em dados quentes: leituras e escritas frequentes sobre os mesmos registros geram contenção de locks no banco, degradando a performance dos dois lados simultaneamente.
Estrutura
O fluxo central do CQRS separa completamente o caminho de um Command do caminho de uma Query. Ambos entram pelo mesmo ponto de entrada (ex.: um controller HTTP), mas seguem por handlers e modelos distintos.
┌─────────────────────────────────────────────────────────────────────┐
│ PONTO DE ENTRADA (Controller / API) │
└────────────────────┬────────────────────────┬────────────────────────┘
│ Command │ Query
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ CommandHandler │ │ QueryHandler │
│ │ │ │
│ - valida regras │ │ - lê dados │
│ - muta estado │ │ - projeta DTO │
│ - emite eventos │ │ - nunca muta estado │
└────────┬─────────┘ └──────────┬─────────────┘
│ persiste │ lê de
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ Write Model │ │ Read Model │
│ │ │ │
│ Agregados ricos │ │ Projeções planas │
│ invariantes │ │ desnormalizadas │
│ transações ACID │ │ otimizadas por query │
└──────────────────┘ └──────────────────────┘
Graus de separação:
Simples — mesmo banco, dois modelos no código (ORM distinto por lado)
Completo — bancos distintos; Read Model sincronizado via eventos ou polling
Máximo — CQRS + Event Sourcing; Read Model derivado dos eventos do stream
Os três graus de aplicação
- Simples (mesmo banco): o Write Model usa o ORM completo com agregados ricos; o Read Model usa queries SQL diretas ou views materializadas no mesmo banco. É o ponto de entrada mais comum e traz boa parte do benefício com pouca complexidade adicional.
- Completo (bancos separados): o Write Model persiste em um banco relacional ou orientado a documentos; o Read Model é mantido em um banco otimizado para leitura (ex.: Elasticsearch, Redis, réplica de leitura, tabela projeção). Cada evento de domínio ou mudança de estado desencadeia a atualização do Read Model — introduzindo consistência eventual.
- Máximo (CQRS + Event Sourcing): o Write Model persiste eventos imutáveis em vez do estado atual. O Read Model é derivado inteiramente desses eventos. O estado atual é reconstruído a partir do event stream. CQRS e Event Sourcing são ortogonais — um pode existir sem o outro — mas se combinam naturalmente aqui.
Como funciona
O trecho abaixo mostra a estrutura mínima de um CommandHandler e um QueryHandler. Note que o Command não retorna dados de domínio — apenas um identificador de confirmação ou nada. O QueryHandler nunca toca no Write Model.
// ── COMMAND: expressa intenção de mudança ───────────────────
interface CriarPedidoCommand {
readonly clienteId: string;
readonly itens: { produtoId: string; quantidade: number }[];
}
// ── COMMAND HANDLER: valida, muta, persiste ──────────────────
class CriarPedidoHandler {
constructor(private readonly repo: PedidoRepository) {}
async handle(cmd: CriarPedidoCommand): Promise<{ pedidoId: string }> {
const pedido = Pedido.criar(cmd.clienteId, cmd.itens); // regras de negócio
await this.repo.salvar(pedido);
return { pedidoId: pedido.id }; // só o ID — sem projeção de leitura
}
}
// ── QUERY: pergunta ao sistema ───────────────────────────────
interface ListarPedidosQuery {
readonly clienteId: string;
readonly pagina: number;
}
// ── QUERY HANDLER: lê, projeta, nunca muta ──────────────────
class ListarPedidosHandler {
constructor(private readonly db: ReadDatabase) {}
async handle(query: ListarPedidosQuery): Promise<PedidoResumoDTO[]> {
// SQL direto, view ou índice otimizado — sem passar pelo agregado
return this.db.query(
'SELECT id, status, total FROM pedidos_view WHERE cliente_id = $1 LIMIT 20 OFFSET $2',
[query.clienteId, query.pagina * 20],
);
}
}
A separação é especialmente visível no tipo de retorno: o handler de Command devolve apenas dados de correlação (o ID gerado); o handler de Query devolve um DTO de leitura montado pela projeção, sem expor nenhum agregado de domínio.
Quando usar
- Carga de leitura muito maior que escrita: separar os modelos permite escalar o lado de leitura independentemente — réplicas de banco, caches dedicados, índices especializados — sem afetar as garantias de consistência do lado de escrita.
- Domínio rico com invariantes complexas: quando o modelo de escrita precisa proteger regras de negócio sérias (limites de crédito, estoques não negativos, transições de estado válidas), manter esse modelo separado das queries evita que otimizações de leitura introduzam brechas nas invariantes.
- Consultas com formatos muito distintos do modelo de domínio: dashboards, relatórios e telas de listagem frequentemente precisam de projeções desnormalizadas que não existem no modelo de domínio. Com CQRS, o Read Model pode ser projetado exatamente para cada caso de uso de leitura.
- Sistemas que já usam Event Sourcing: o event stream produzido pelo Write Model é a fonte natural para alimentar projeções de leitura especializadas — o CQRS completo emerge de forma quase natural.
Quando evitar
- CRUDs simples sem regras de domínio: quando não há invariantes a proteger nem consultas complexas, CQRS adiciona camadas de indireção sem benefício real. Uma Arquitetura em Camadas simples ou um repositório genérico resolvem o problema com muito menos código.
- Equipes pequenas sem experiência com consistência eventual: o grau completo (bancos separados) exige lidar com Read Models desatualizados, estratégias de sincronização e lógica de compensação. O custo de entender e operar esse modelo é alto.
- Quando a consistência imediata é obrigatória: se a UI precisa mostrar exatamente o estado após cada mutação, a consistência eventual do Read Model exige workarounds (polling, otimistic UI, retorno do estado no Command response) que reduzem o valor da separação.
Prós e contras
Prós
- Modelos otimizados para sua função: o Write Model protege invariantes; o Read Model entrega projeções eficientes.
- Escalabilidade assimétrica: leitura e escrita escalam independentemente, com infraestrutura adequada para cada carga.
- Queries sem fricção com o domínio: o Read Model pode usar SQL direto, views materializadas ou índices especializados sem comprometer a modelagem do domínio.
- Auditoria e rastreabilidade naturais: Commands carregam intenção explícita e podem ser logados ou persistidos como log de mudanças.
- Combinação natural com Event Sourcing: o event stream do Write Model alimenta projeções de Read Model de forma desacoplada.
Contras
- Mais código e mais conceitos: dois modelos, dois sets de handlers, possivelmente dois bancos — o volume de código e a quantidade de conceitos crescem significativamente.
- Consistência eventual (no grau completo): o Read Model pode estar desatualizado após uma mutação. Isso exige que a UI e o negócio aceitem janelas de inconsistência.
- Sincronização do Read Model: manter o Read Model atualizado requer uma estratégia explícita — eventos de domínio, change data capture ou polling — que precisa ser monitorada e operada.
- Debug mais complexo: rastrear por que uma Query retornou dados inesperados exige verificar tanto o estado do Write Model quanto o estado do Read Model e o mecanismo de sincronização entre eles.
Armadilhas comuns
1. Confundir CQRS com Event Sourcing
CQRS e Event Sourcing são conceitos ortogonais. CQRS separa modelos de leitura e escrita — o Write Model pode persistir o estado atual, como qualquer sistema convencional. Event Sourcing é uma estratégia de persistência onde o estado é derivado de uma sequência imutável de eventos. Os dois se combinam bem, mas nenhum implica o outro. Aplicar CQRS sem Event Sourcing é plenamente válido e muito mais simples. Aplicar Event Sourcing sem CQRS é igualmente possível (e igualmente raro de fazer bem).
Regra prática: comece com CQRS simples (mesmo banco, dois modelos no código). Só avance para bancos separados ou Event Sourcing quando tiver um problema mensurável que justifique a complexidade adicional.
2. Read Model desatualizado causando bugs sutis
No grau completo, há uma janela de tempo entre a persistência do Write Model e a atualização do Read Model. Se a UI faz uma Query imediatamente após um Command e exibe o resultado, o usuário pode ver o estado anterior. Isso não é um bug do CQRS — é uma propriedade do modelo — mas precisa ser comunicada claramente ao produto e tratada na UI. Soluções comuns incluem retornar o ID e o novo estado resumido no response do Command, usar otimistic updates na UI ou esperar uma confirmação via polling.
3. Aplicar CQRS onde um CRUD resolve
O custo de introduzir CommandHandlers, QueryHandlers, dois modelos e uma estratégia de sincronização em uma aplicação que apenas cria, lê, atualiza e deleta registros sem regras de domínio complexas é custo real sem retorno mensurável. CQRS não é um padrão de organização de código — é um padrão para resolver problemas específicos de escalabilidade e modelagem. Aplique onde o problema existe, não como convenção global do projeto.
4. Misturar lógica de escrita no QueryHandler
A separação só tem valor se for mantida com disciplina. QueryHandlers que atualizam contadores de visualização, registram logs de acesso ou alteram qualquer estado colapsam a separação e tornam as queries imprevisíveis e não idempotentes. Se uma operação de leitura precisa produzir um efeito colateral, esse efeito deve ser modelado como um Command separado, disparado após a Query ou em paralelo.
5. Sincronização frágil do Read Model
Um Read Model que é atualizado de forma síncrona dentro da transação de escrita anula os benefícios de desacoplamento. Um que é atualizado via evento assíncrono sem garantias de entrega, sem idempotência no handler e sem monitoramento de lag cria inconsistências silenciosas difíceis de depurar. A sincronização do Read Model é uma parte crítica da arquitetura e precisa de atenção equivalente à dos modelos em si.
Arquiteturas e padrões relacionados
A Event-Driven Architecture é o mecanismo mais comum para sincronizar o Read Model no grau completo do CQRS. Quando o CommandHandler persiste um Command, ele também publica um evento de domínio; um consumidor do lado de leitura recebe esse evento e atualiza o Read Model. Os dois padrões se complementam, mas são independentes: EDA não exige CQRS, e CQRS no grau simples não exige EDA.
A Clean Architecture e o CQRS se encaixam naturalmente: CommandHandlers e QueryHandlers mapeiam para Use Case Interactors; o Write Model vive na camada de domínio; o Read Model vive na camada de adaptadores ou infraestrutura. A regra da dependência é preservada — os handlers definem interfaces de repositório que a infraestrutura implementa.
Em Microsserviços, o CQRS com bancos separados é especialmente valioso porque cada serviço pode manter seu próprio Read Model projetado para suas necessidades, evitando joins entre serviços. A sincronização ocorre via eventos publicados no barramento inter-serviços.