CQRS: o que é o padrão Command Query Responsibility Segregation

CQRS: o que é o padrão Command Query Responsibility Segregation

Se você já trabalhou num sistema que começou pequeno e foi crescendo, provavelmente viveu esta cena: aquela mesma entidade que era só um Pedido com meia dúzia de campos virou um monstro. Ela precisa validar regras de negócio na hora de salvar, mas também precisa aparecer em quinze telas diferentes, cada uma pedindo um recorte distinto dos dados, algumas com JOINs pesados, outras com dados agregados. Aí a gente enfia tudo no mesmo modelo, na mesma classe, no mesmo repositório, e o que era simples começa a ranger.

O CQRS nasceu pra endereçar exatamente esse desconforto. É um dos padrões mais citados em conversas sobre arquitetura moderna, junto com event sourcing e microsserviços, mas também é um dos mais mal-entendidos. Muita gente acha que precisa de dois bancos de dados, filas, eventos e uma pilha inteira de infraestrutura só pra “fazer CQRS”. Não precisa. Neste post a gente vai destrinchar o que o padrão realmente é, o problema concreto que ele resolve, como ele se relaciona com event sourcing, um exemplo de código de verdade, e (talvez o mais importante) quando você definitivamente não deveria usar.

O que é CQRS

CQRS é a sigla de Command Query Responsibility Segregation, ou seja, segregação de responsabilidade entre comandos e consultas. A ideia central é simples: você separa as operações que alteram estado (os comandos, ou commands) das operações que apenas leem estado (as consultas, ou queries), tratando cada lado como um caminho independente dentro da aplicação, com modelos, objetos e às vezes até bancos de dados próprios.

Num sistema tradicional, você tem um único modelo que faz as duas coisas. A classe Produto tanto sabe se salvar quanto sabe se apresentar. No CQRS, esse modelo único se quebra em dois: um modelo de escrita, otimizado pra consistência e regras de negócio, e um modelo de leitura, otimizado pra velocidade e pro formato que a tela precisa. O termo foi cunhado por Greg Young por volta de 2010, mas ele mesmo faz questão de dizer que é só uma evolução do princípio CQS (Command Query Separation) do Bertrand Meyer, que já dizia nos anos 80 que um método ou muda o estado ou retorna um valor, nunca as duas coisas ao mesmo tempo.

A diferença entre CQS e CQRS é de escala. O CQS fala de métodos individuais. O CQRS pega esse princípio e sobe ele pro nível arquitetural: não são só os métodos que se separam, são os caminhos inteiros de leitura e escrita, cada um com seu próprio conjunto de classes e responsabilidades.

O problema que o CQRS resolve

Pra entender o valor do padrão, vale olhar pro sintoma que ele ataca. Em aplicações com CRUD clássico, leitura e escrita compartilham o mesmo modelo de domínio. No começo isso é ótimo, menos código, menos duplicação. O problema aparece quando as necessidades dos dois lados divergem, e elas quase sempre divergem.

Do lado da escrita, o que importa é integridade. Você quer garantir que um pedido não seja finalizado sem itens, que o estoque não fique negativo, que o total bata com a soma das linhas. Isso pede um modelo rico, com validações, invariantes protegidas, transações. Do lado da leitura, nada disso importa. Você só quer jogar dados na tela o mais rápido possível, muitas vezes desnormalizados, agregados, formatados de um jeito que nenhuma regra de negócio se preocupa em manter.

Quando esses dois mundos vivem no mesmo modelo, um atrapalha o outro. As entidades ficam inchadas de propriedades que só existem pra alimentar alguma tela específica. Os repositórios acumulam dezenas de métodos buscarPorXParaTelaY. O ORM começa a carregar grafos gigantes de objetos só pra você usar três campos. E qualquer mudança na leitura corre o risco de quebrar a escrita, porque estão acoplados na mesma classe. O CQRS corta esse nó ao dizer: para de tentar servir os dois senhores com o mesmo objeto.

CQRS: o que é o padrão Command Query Responsibility Segregation ilustracao 1

Separando command e query na prática

Na aplicação, essa separação se materializa em dois tipos de objetos bem definidos. Um command é uma intenção de mudar algo: CriarPedido, CancelarAssinatura, AtualizarEndereco. Ele carrega os dados necessários pra executar a operação e, por convenção, não devolve dados de negócio, no máximo um identificador ou um resultado de sucesso/falha. Uma query é uma pergunta: BuscarPedidosDoCliente, ObterResumoDoDashboard. Ela nunca altera nada e sempre devolve dados, geralmente num formato pronto pra consumo (um DTO, um view model), não a entidade de domínio.

Cada command é tratado por um handler dedicado, e cada query também. Isso deixa cada operação isolada numa unidade pequena e testável, em vez de um service gigante com trinta métodos. Vale um exemplo em C#, que é onde esse estilo pegou bem por causa da biblioteca MediatR:

// Lado da ESCRITA: um command e seu handler
public record CriarPedidoCommand(int ClienteId, List<ItemDto> Itens) : IRequest<int>;

public class CriarPedidoHandler : IRequestHandler<CriarPedidoCommand, int>
{
    private readonly IPedidoRepository _repo;

    public async Task<int> Handle(CriarPedidoCommand cmd, CancellationToken ct)
    {
        var pedido = Pedido.Criar(cmd.ClienteId, cmd.Itens); // valida invariantes
        await _repo.Salvar(pedido);
        return pedido.Id; // devolve so o id, nada de dado de leitura
    }
}

// Lado da LEITURA: uma query e seu handler, sem passar pelo dominio
public record ResumoPedidoQuery(int PedidoId) : IRequest<ResumoPedidoView>;

public class ResumoPedidoHandler : IRequestHandler<ResumoPedidoQuery, ResumoPedidoView>
{
    private readonly IDbConnection _db;

    public async Task<ResumoPedidoView> Handle(ResumoPedidoQuery q, CancellationToken ct)
    {
        // SQL direto, projetando so o que a tela precisa
        return await _db.QuerySingleAsync<ResumoPedidoView>(
            "SELECT id, cliente_nome, total, status FROM vw_resumo_pedido WHERE id = @Id",
            new { Id = q.PedidoId });
    }
}

Repare numa coisa que costuma passar batido: o lado da leitura nem toca no repositório de domínio nem no ORM. Ele vai direto no banco com uma query enxuta e projeta o resultado num ResumoPedidoView. Isso é totalmente permitido no CQRS e, na minha experiência, é onde mora boa parte do ganho de performance. Você para de instanciar agregados inteiros só pra montar uma listagem. A escrita continua rica e protegida, a leitura fica magra e rápida, e nenhuma das duas atrapalha a outra.

Um ponto que gera confusão: essa separação não obriga bancos de dados diferentes. No exemplo acima, escrita e leitura conversam com o mesmo banco. O que muda é o caminho lógico dentro do código. Bancos separados são uma opção que você pode adotar depois, se e quando a pressão de leitura justificar, e é aí que a coisa fica interessante.

CQRS e event sourcing: parentes, não gêmeos

Poucos assuntos causam mais confusão do que a relação entre CQRS e event sourcing. As duas coisas aparecem tanto juntas em palestras e artigos que muita gente acredita que uma exige a outra. Não exige. São padrões independentes que se dão bem, mas cada um funciona perfeitamente sozinho.

Event sourcing é uma forma de persistir estado. Em vez de guardar o estado atual de uma entidade (uma linha na tabela pedidos com o status corrente), você guarda a sequência de eventos que levaram até ali: PedidoCriado, ItemAdicionado, PedidoPago, PedidoEnviado. O estado atual é reconstruído aplicando esses eventos em ordem. É como um extrato bancário: você não armazena só o saldo, você armazena cada transação, e o saldo é a soma delas.

A razão de os dois andarem juntos é natural. Quando você usa event sourcing, reconstruir o estado a partir de eventos toda vez que alguém quer ler algo seria lento e impraticável em telas de listagem. Então você projeta esses eventos em modelos de leitura otimizados, tabelas planas, prontas pra consulta. Ou seja, o event sourcing praticamente te empurra pra ter um lado de leitura separado, que é exatamente o que o CQRS propõe. Um resolve como você grava, o outro resolve como você separa gravação de consulta. Casam bem, mas não são a mesma coisa, e você pode (e na maioria dos casos deveria) começar só com CQRS sem chegar perto de event sourcing.

CQRS: o que é o padrão Command Query Responsibility Segregation ilustracao 2

Modelos assíncronos e consistência eventual

Quando você leva o CQRS até o ponto de ter armazenamentos de leitura e escrita fisicamente separados, aparece um novo personagem na história: a consistência eventual. A escrita acontece no banco de comando, e um mecanismo qualquer (um processo que escuta eventos, uma fila, um job) atualiza o banco de leitura logo depois. Esse “logo depois” pode ser milissegundos ou pode ser alguns segundos.

Na prática isso significa que existe uma janela em que o usuário salvou algo mas a tela de leitura ainda não reflete a mudança. Ele clica em “salvar”, volta pra listagem, e o item novo não está lá porque a projeção ainda não rodou. Isso não é bug, é a natureza do modelo, e é uma das partes mais subestimadas do custo do CQRS. A interface precisa ser pensada pra lidar com esse atraso, seja mostrando um estado otimista, seja avisando o usuário, seja escondendo casos onde a defasagem seria inaceitável.

Por isso a versão “simples” do CQRS, com um único banco e separação só no código, é tão popular. Ela te dá quase toda a clareza da separação de responsabilidades sem te obrigar a pagar o pedágio da consistência eventual. Você só assume esse custo quando a escala realmente pede.

CRUD tradicional versus CQRS lado a lado

Pra deixar concreto onde cada abordagem brilha e onde tropeça, comparar os dois modelos diretamente ajuda:

Aspecto CRUD tradicional CQRS
Modelo de dados Um único modelo pra ler e escrever Modelo de escrita e modelo de leitura separados
Otimização Compromisso entre leitura e escrita Cada lado otimizado pra sua função
Complexidade inicial Baixa, fácil de começar Mais alta, mais peças móveis
Escalabilidade de leitura Limitada, presa ao modelo de escrita Alta, leitura escala de forma independente
Consistência Forte e imediata Forte no simples, eventual quando há bancos separados
Ideal para Cadastros, admin, sistemas simples Domínios complexos, muita leitura, alto throughput

Vantagens do CQRS

Quando o padrão encaixa no problema certo, os ganhos são reais e sentidos no dia a dia. O primeiro é a separação de responsabilidades de verdade. Cada lado do sistema tem uma única razão pra mudar. Alterar como uma tela mostra dados não coloca em risco as regras de negócio, porque elas vivem em outro lugar. Isso baixa o medo de mexer no código, que é um ganho difícil de medir mas enorme na prática.

O segundo é performance de leitura. Ao modelar consultas com objetos dedicados, você projeta exatamente o que precisa, foge de over-fetching do ORM e pode até escalar o lado de leitura separadamente, com réplicas, caches ou bancos especializados. O terceiro é a clareza do modelo de escrita. Livre do peso de servir consultas, o domínio fica focado em proteger invariantes e expressar regras de negócio, o que costuma resultar em código mais limpo e alinhado com o que o negócio realmente faz.

Tem ainda um ganho de organização que aparece com handlers isolados. Cada operação vira uma unidade pequena, fácil de encontrar, fácil de testar em isolamento. Em vez de um PedidoService com dois mil linhas, você tem vários arquivos curtos, cada um resolvendo uma coisa. Times grandes se beneficiam muito disso porque conflitos de merge diminuem e a navegação no código fica óbvia.

O custo de complexidade

Agora a parte honesta. CQRS não é de graça, e o preço é complexidade. A crítica mais comum ao padrão, e ela procede, é que muita gente o adota por moda, achando que está fazendo arquitetura séria, quando na verdade está só adicionando cerimônia a um problema que não precisava dela.

O custo mais óbvio é a duplicação. Você passa a ter dois modelos pro mesmo conceito, e uma mudança de negócio pode exigir tocar em ambos. Tem também a curva de aprendizado: um desenvolvedor novo no time precisa entender por que existem commands, queries e handlers espalhados, e por que ele não pode simplesmente chamar um método no service como sempre fez. E se você for pro CQRS com armazenamentos separados, a consistência eventual traz uma classe inteira de problemas novos de sincronização, ordenação de eventos, reprocessamento de projeções, monitoramento da defasagem. Nada disso existe num CRUD comum.

A minha regra pessoal é essa: comece sem CQRS. Deixe o CRUD simples te servir enquanto ele servir. Você vai perceber sozinho quando o modelo único começar a doer, quando os métodos de consulta no repositório virarem uma bagunça, quando a leitura e a escrita brigarem por otimizações incompatíveis. Esse desconforto é o sinal. Introduzir CQRS como resposta a uma dor concreta é sábio. Introduzir CQRS por precaução, num sistema que ainda nem sabe onde vai doer, quase sempre é desperdício.

Quando NÃO usar CQRS

Existem cenários em que o padrão é claramente a ferramenta errada, e reconhecê-los te economiza muito sofrimento. Se o seu sistema é essencialmente um CRUD, cadastros, telas administrativas, formulários que salvam e listam sem lógica de negócio elaborada, CQRS só vai adicionar camadas que não pagam por si mesmas. Aqui a simplicidade do modelo único ganha de lavada.

Também não vale a pena quando o domínio é pequeno ou o time é pequeno. A sobrecarga cognitiva de manter dois modelos separados só compensa quando a complexidade do negócio ou o volume de acessos justifica. Num projeto enxuto, essa mesma sobrecarga vira atrito puro. E se as necessidades de leitura e escrita são praticamente idênticas, os dados que você grava são os mesmos que você lê, sem transformações nem agregações, então não existe divergência pra separação resolver, e o CQRS fica sem propósito.

Uma armadilha comum é aplicar CQRS de forma global, no sistema inteiro. Isso quase nunca é boa ideia. O padrão brilha quando aplicado cirurgicamente em um bounded context específico, aquele módulo de fato complexo, com muita leitura ou regras densas, enquanto o resto do sistema segue com CRUD tranquilo. Arquitetura não precisa ser uniforme. Usar CQRS onde ele resolve e CRUD onde ele basta é sinal de maturidade, não de inconsistência.

Perguntas frequentes sobre CQRS

CQRS exige dois bancos de dados?

Não. Essa é a confusão mais comum sobre o padrão. CQRS é sobre separar os modelos de leitura e escrita no nível do código, com commands, queries e seus handlers. Você pode fazer isso apontando ambos os lados pro mesmo banco de dados. Bancos separados são uma otimização opcional que faz sentido quando a carga de leitura é muito alta e você quer escalar essa parte de forma independente, mas é um passo posterior, não um pré-requisito.

Qual a diferença entre CQRS e CQS?

CQS (Command Query Separation) é um princípio de design no nível de métodos: um método ou altera estado ou retorna dados, nunca os dois. CQRS pega essa ideia e a eleva ao nível arquitetural, separando não apenas métodos, mas modelos, objetos e caminhos de dados inteiros de leitura e escrita. Todo CQRS respeita o CQS, mas nem todo código que segue CQS chega a ser CQRS.

CQRS obriga usar event sourcing?

Não. São padrões independentes. Event sourcing trata de como você persiste dados (guardando eventos em vez de estado atual), enquanto CQRS trata de como você separa leitura de escrita. Eles combinam muito bem, porque o event sourcing naturalmente pede modelos de leitura separados, mas você pode adotar CQRS com um banco relacional comum e sem nenhum evento, e é assim que a maioria dos projetos deveria começar.

CQRS serve para microsserviços?

Combina bem, mas não é obrigatório nem exclusivo. Em arquiteturas de microsserviços, o CQRS ajuda a separar serviços de leitura de serviços de escrita e a lidar com dados distribuídos, muitas vezes ao lado de mensageria assíncrona. Mas você pode usar CQRS tranquilamente dentro de um monólito, aplicado a um módulo específico. O padrão não tem nada a ver com a forma como você distribui a aplicação.

CQRS deixa o código mais lento?

Na escrita, o overhead é desprezível. Na leitura, quase sempre o CQRS deixa mais rápido, porque você projeta consultas dedicadas que buscam só o necessário, em vez de carregar agregados inteiros pelo ORM. O custo do padrão não é de performance de execução, é de complexidade de manutenção: mais arquivos, mais conceitos, mais peças pra entender. É esse custo que você precisa pesar, não o de velocidade.

Vale a pena usar CQRS em um projeto pequeno?

Na maioria dos casos, não. Projetos pequenos, com domínio simples e volume baixo, são melhor servidos por um CRUD direto. A complexidade extra do CQRS só se paga quando existe divergência real entre as necessidades de leitura e escrita, ou quando a escala de acessos exige otimizações independentes. Adotar o padrão cedo demais, sem uma dor concreta pra justificar, costuma ser mais atrapalho do que ajuda.

Conclusão

CQRS é uma ferramenta afiada, e como toda ferramenta afiada, ela resolve muito bem os problemas pra que foi feita e machuca quando usada fora de contexto. O coração do padrão é uma ideia quase óbvia depois que você a enxerga: leitura e escrita têm necessidades diferentes, então trate cada uma no seu próprio caminho, com seus próprios modelos. Isso te dá um domínio de escrita limpo e protegido, um lado de leitura rápido e flexível, e a liberdade de evoluir cada um sem medo de quebrar o outro.

O que separa quem usa bem de quem só complica é o timing e o escopo. Comece simples, com CRUD, e deixe a dor real do seu sistema te dizer onde a separação vale a pena. Aplique o padrão cirurgicamente, num contexto que de fato precise dele, e resista à tentação de espalhá-lo pelo sistema inteiro só porque parece sofisticado. Guardar o event sourcing pra quando ele fizer sentido, e não antes, faz parte da mesma disciplina. CQRS bem aplicado é quase invisível: o sistema simplesmente fica mais fácil de entender e mais rápido de ler.

Para quem constrói sistemas que precisam de dados oficiais, o Hub do Desenvolvedor mantém APIs de consulta de CNPJ e CPF na Receita Federal prontas para você integrar sem burocracia.

Compartilhe nas mídias: