CQRS
A ideia
Seção intitulada “A ideia”O agregado existe para proteger invariantes na escrita: carregar o cluster inteiro, decidir, salvar consistente. Uma tela de listagem não muda estado nenhum — forçá-la a passar pelo agregado significa hidratar grafos inteiros para jogar fora, e distorcer o modelo com métodos de consulta que não protegem nada.
CQRS oficializa a assimetria: comandos passam pelo modelo rico; consultas têm caminho próprio, otimizado para leitura, devolvendo DTOs no formato da tela.
graph LR
CMD["Comando<br/>confirmar pedido"] --> UC["Use case"] --> AGG["Agregado<br/>Pedido"] --> DB[("Banco")]
QRY["Consulta<br/>listar resumos"] --> QH["Query handler"] --> DB
QH --> DTO["PedidoResumo (DTO)"]
Níveis de adoção
Seção intitulada “Níveis de adoção”| Nível | O que separa | Quando |
|---|---|---|
| 0 | Nada: mesmo modelo para tudo | CRUDs simples |
| 1 | Consultas dedicadas no mesmo banco (DTO via SQL/projeção) | A maioria dos sistemas com telas de listagem e relatórios |
| 2 | Modelo de leitura materializado (projeções alimentadas por Domain Events) | Leitura com formato muito diferente da escrita, alto volume |
| 3 | Event Sourcing + stores separados | Casos raros com exigência de auditoria total/replay; custo alto |
O nível 1 resolve a maior parte da dor e é o referenciado por Repositórios (“consultas complexas podem escapar do repositório de escrita”) e Agregados.
Exemplo em Java + Spring (nível 1)
Seção intitulada “Exemplo em Java + Spring (nível 1)”A consulta é uma porta de saída de leitura, com DTO próprio — o agregado Pedido nunca é carregado:
package com.loja.pedido.application;
import java.math.BigDecimal;import java.util.List;
public record PedidoResumo(String pedidoId, String status, BigDecimal total, int quantidadeItens) {}
public interface ConsultaPedidos { List<PedidoResumo> resumosDoCliente(String clienteId);}package com.loja.pedido.infrastructure.persistence;
import com.loja.pedido.application.ConsultaPedidos;import com.loja.pedido.application.PedidoResumo;import java.util.List;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Component;
@Componentpublic class ConsultaPedidosJdbc implements ConsultaPedidos {
private final JdbcTemplate jdbcTemplate;
public ConsultaPedidosJdbc(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; }
@Override public List<PedidoResumo> resumosDoCliente(String clienteId) { return jdbcTemplate.query(""" SELECT p.id, p.status, p.total, COUNT(i.id) AS itens FROM pedido p JOIN item_pedido i ON i.pedido_id = p.id WHERE p.cliente_id = ? GROUP BY p.id, p.status, p.total """, (rs, linha) -> new PedidoResumo( rs.getString("id"), rs.getString("status"), rs.getBigDecimal("total"), rs.getInt("itens")), clienteId); }}Não há perda em pular o agregado: consulta não precisa de invariante. E a interface continua sendo uma driven port (Ports) — o SQL fica no adapter.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- CQRS = event sourcing + 2 bancos: confundir o padrão com sua versão mais cara. Os níveis 1-2 não exigem nada disso.
- Query passando pelo agregado por dogma: hidratar o cluster inteiro para montar uma listagem paginada.
- Escrita via modelo de leitura: atualizar direto a projeção/DTO, contornando as invariantes do agregado — o lado de escrita continua sendo o único que muda estado.
- Aplicar em CRUD trivial: sem assimetria real entre leitura e escrita, a separação é cerimônia.
Relações
Seção intitulada “Relações”- Libera a leitura da rigidez de Agregados e Repositórios
- Projeções de leitura (nível 2) alimentadas por Domain Events
- Consultas são driven ports: Ports, Adapters
- Parte do vocabulário de Domain-Driven Design
- Voltar para Home
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. O que exatamente o CQRS separa?
Resposta
A responsabilidade de escrita (comandos que passam pelos agregados e protegem invariantes) da de leitura (consultas que produzem DTOs otimizados para a tela, sem carregar agregados).
2. CQRS exige event sourcing ou dois bancos?
Resposta
Não. O nível mais útil e comum é o 1: consultas dedicadas no mesmo banco, com SQL/projeções próprias. Event sourcing e stores separados são o nível mais caro, para casos específicos.
3. Por que consultas não precisam passar pelo agregado?
Resposta
Porque o agregado existe para proteger invariantes em mudanças de estado. Uma consulta não muda nada — carregá-lo é custo sem benefício, e ainda distorce o modelo com métodos de leitura.
4. Qual o risco de escrever pelo modelo de leitura?
Resposta
Contornar as invariantes: a projeção/DTO não valida nada. Toda mudança de estado deve continuar entrando pelo lado de comando, via raiz de agregado.