Software · Arquitetura de Software
Padrão CQRS: Segregação de Responsabilidade entre Comandos e Consultas
Como o padrão CQRS separa os modelos de escrita e leitura para otimizar desempenho, viabilizar interfaces baseadas em tarefas e suportar alta concorrência.
Neste artigo
Em arquiteturas convencionais orientadas a CRUD, o mesmo modelo de entidade é utilizado tanto para validar mutações complexas de estado quanto para alimentar telas e relatórios no navegador. À medida que o sistema cresce, essa convergência gera atritos graves: consultas de visualização passam a exigir junções complexas em tabelas normalizadas, enquanto operações simples de atualização precisam carregar grafos inteiros de relacionamentos que nada têm a ver com a mutação em andamento.
O padrão CQRS (Command Query Responsibility Segregation), formulado por Greg Young a partir do princípio CQS de Bertrand Meyer, resolve esse conflito dividindo a arquitetura em dois canais autônomos: um canal dedicado estritamente a alterar o estado do sistema e outro desenhado especificamente para extrair informações com máxima velocidade.
Anatomia do CQRS: Comandos versus Consultas
A segregação divide as operações do sistema em duas categorias com objetivos funcionais opostos:
graph TD
UI["<b>Interface do Usuário (UI)</b>"]
UI -->|Comando de Escrita| CB["<b>Command Bus</b>"]
UI -->|Consulta de Leitura| QH["<b>Query Handler</b>"]
CB --> MW["<b>Modelo de Escrita</b><br/>(Valida Invariantes & Domínio)"]
MW -->|Grava Transação| DB_W[("<b>Banco de Escrita</b><br/>(Normalizado / ACID)")]
DB_W -.->|Eventos de Domínio| Proj["<b>Projeção Assíncrona</b>"]
Proj -.->|Atualiza Visões| DB_R[("<b>Banco de Leitura</b><br/>(Otimizado / Desnormalizado)")]
QH -->|Lê DTOs Diretos| DB_R
DB_R -->|Retorna Dados| UI
classDef ui fill:#0f172a,stroke:#64748b,stroke-width:1px,color:#f8fafc;
classDef write fill:#1e1b4b,stroke:#a855f7,stroke-width:1px,color:#f8fafc;
classDef read fill:#0f2724,stroke:#34d399,stroke-width:1px,color:#f8fafc;
classDef db fill:#132338,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
class UI ui;
class CB,MW write;
class QH,Proj read;
class DB_W,DB_R db;
1. Comandos (Commands)
Um comando expressa uma intenção explícita de negócio (por exemplo, AprovarPedido, ReajustarLimiteDeCredito), capturada por uma interface orientada a tarefas em vez de formulários genéricos de edição de colunas. O comando é despachado através de um mecanismo estrutural (Command Bus) até seu respectivo handler.
O manipulador carrega o agregado de negócio, valida invariantes críticas, aplica as alterações e grava o novo estado no banco de escrita. O comando não deve retornar modelos enriquecidos de domínio; ele confirma o processamento e devolve no máximo o identificador gerado ou o status da operação.
2. Consultas (Queries)
Uma consulta requisita dados sem produzir nenhum efeito colateral sobre o sistema. No lado de leitura, o sistema não instancia entidades de domínio nem valida regras de transição. Em vez disso, o manipulador de consulta busca tabelas desnormalizadas ou visualizações materiais e preenche diretamente objetos de transferência (DTOs), eliminando a sobrecarga de mapeadores e rastreamento de instâncias pelo ORM.
Implementação Tática com Segregação em Código
Na prática, a divisão pode ser estruturada através de bibliotecas de mediação como o MediatR em ecossistemas .NET ou manipuladores de barramento equivalentes em TypeScript e Go:
// 1. Comando: Expressa intenção de escrita com dados de entrada
public record CancelarMatriculaCommand(Guid AlunoId, string Motivo) : IRequest<bool>;
public class CancelarMatriculaHandler : IRequestHandler<CancelarMatriculaCommand, bool>
{
private readonly WriteDbContext _db;
public CancelarMatriculaHandler(WriteDbContext db) => _db = db;
public async Task<bool> Handle(CancelarMatriculaCommand cmd, CancellationToken ct)
{
var aluno = await _db.Alunos.FindAsync(new object[] { cmd.AlunoId }, ct);
if (aluno == null) throw new InvalidOperationException("Aluno não encontrado.");
aluno.CancelarMatricula(cmd.Motivo);
await _db.SaveChangesAsync(ct);
return true;
}
}
// 2. Consulta: Projeção de dados direta para a interface
public record ObterResumoAlunoQuery(Guid AlunoId) : IRequest<AlunoResumoDto?>;
public class ObterResumoAlunoHandler : IRequestHandler<ObterResumoAlunoQuery, AlunoResumoDto?>
{
private readonly ReadDbContext _db;
public ObterResumoAlunoHandler(ReadDbContext db) => _db = db;
public async Task<AlunoResumoDto?> Handle(ObterResumoAlunoQuery query, CancellationToken ct)
{
return await _db.Alunos
.AsNoTracking()
.Where(a => a.Id == query.AlunoId)
.Select(a => new AlunoResumoDto(a.Id, a.Nome, a.Status, a.TotalCreditos))
.FirstOrDefaultAsync(ct);
}
}
Essa diferenciação reduz o consumo de memória no servidor web, pois as consultas não acumulam instâncias no gerenciador de ciclo de vida do ORM e podem utilizar índices de leitura específicos.
Sincronização e Consistência Eventual
O CQRS pode ser adotado compartilhando o mesmo banco de dados relacional entre escrita e leitura (CQRS Lógico), ou separando fisicamente os armazenamentos em tecnologias distintas (CQRS Físico):
- Banco de Escrita: Geralmente um banco relacional transacional (PostgreSQL, SQL Server) focado em consistência imediata (ACID) e normalização para evitar duplicidade de dados críticos.
- Banco de Leitura: Armazenamentos otimizados para consulta rápida (Elasticsearch para busca textual, Redis para cache de chave-valor, ou réplicas com esquemas achatados).
Quando bases físicas diferentes são adotadas, a sincronização é conduzida de forma assíncrona por eventos de domínio emitidos após a conclusão do comando. Os projetores de leitura recebem o evento e atualizam os documentos de leitura. A comunicação assíncrona introduz o modelo de consistência eventual: a leitura pode levar alguns milissegundos para refletir a gravação recém-executada, compromisso amplamente tolerado na maioria dos domínios operacionais quando comparado aos ganhos de escala horizontal.
Quando Adotar e Quando Evitar
A separação de responsabilidades do CQRS não deve ser imposta indiscriminadamente a todos os módulos de uma solução. O padrão é especialmente recomendado quando:
- A taxa de leituras supera amplamente o volume de escritas (por exemplo, 100 leituras para cada gravação), justificando projeções cacheadas e réplicas independentes.
- A lógica de negócio para validação e autorização de mutações é complexa e não deve ser misturada com a formatação exigida por painéis analíticos.
- O projeto precisa de auditoria temporal detalhada, casando naturalmente com o padrão de Event Sourcing.
Por outro lado, em tabelas puramente cadastrais de baixa concorrência, o CQRS duplica o número de classes e manipuladores sem entregar benefício proporcional, cenário onde o padrão CRUD convencional continua sendo a escolha mais econômica.