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.

Diagrama conceitual do padrão CQRS demonstrando a segregação entre comandos de escrita e consultas otimizadas
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:

  1. 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.
  2. 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.
  3. 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.

Explorar este tema no Grafo de Conhecimento