PrincípioIniciante5 min de leitura8 seções1988, Data conhecida

CQS (Command Query Separation)

Todo método deve ser ou um comando que muda estado e não retorna dado, ou uma query que retorna dado sem efeito colateral — nunca os dois ao mesmo tempo.

Em 10 segundos

Um método ou pergunta ou manda — nunca os dois: queries retornam dado sem mudar nada (seguras para repetir e cachear), comandos mudam estado e retornam void. Formulado por Bertrand Meyer, é a semente do CQRS, mas vive no nível do método, não da arquitetura.

  • #meyer
  • #efeito-colateral
  • #design-de-metodos
  • #clareza
Analogia

Perguntar as horas não deveria adiantar o relógio

Existem duas formas de falar com alguém: perguntar ('que horas são?') e mandar ('adiante o relógio em uma hora'). Uma pergunta não deveria mudar o mundo — você pode repeti-la dez vezes e esperar a mesma resposta, sem consequências. Uma ordem muda o mundo, e por isso você a dá com intenção, uma vez. O código quebra quando os dois se misturam: um 'que horas são?' que, de brinde, adianta o relógio, transforma cada leitura inocente em um efeito que ninguém pediu.

01Conceito

O problema

Métodos que retornam um valor E mudam estado escondem seus efeitos colaterais atrás de uma cara de leitura inocente. Quem chama pelo retorno não percebe a mutação; quem chama pela mutação ignora o retorno — e os dois se surpreendem.

Sem a garantia de que ler é inofensivo, você perde liberdades básicas: não pode repetir uma chamada, reordenar duas, nem cachear um resultado sem medo do que acontece no caminho.

A raiz do problema é a assinatura mentirosa: 'number getSaldo()' promete uma observação, mas nada no tipo denuncia que ela também grava, incrementa ou consome algo. O compilador não ajuda, o autocomplete não avisa — só o leitor do corpo do método descobre. CQS devolve a verdade à assinatura: retorna dado, é seguro; retorna void, muda o mundo.

Importante não confundir com CQRS. CQS é um princípio de design de MÉTODO, proposto por Bertrand Meyer no contexto do Eiffel nos anos 80: cada rotina de um objeto é comando ou query. CQRS pega essa mesma ideia e a eleva à arquitetura — serviços, modelos e até bancos separados para escrita e leitura. CQS é gratuito e quase sempre saudável; CQRS é uma decisão arquitetural cara. Adotar CQS não te obriga a nada; é só disciplina de assinatura.

O ganho profundo é raciocínio equacional: queries livres de efeito são referencialmente transparentes — chamá-las é como ler uma variável. Isso destrava otimizações (memoização, chamadas em paralelo, retry sem culpa) e simplifica testes: uma query se testa comparando retorno; um comando se testa observando o estado depois. Quando um método faz os dois, o teste precisa verificar retorno E estado E a interação entre eles a cada chamada.

Como se encaixa
  1. Chamador
  2. ordena (void)Comando depositar()
  3. mutaEstado muda
  4. depois observaQuery saldo()
  5. retornaDado (sem efeito)
Mutação e observação em passos separados: o comando muda e não conta nada; a query conta e não muda nada.
Passo a passoO fluxo, passo a passo
  1. 1

    Classificar a intenção

    A operação pergunta (ler) ou manda (mutar)? Escolha um dos dois papéis.

  2. 2

    Comando = void

    Métodos que mudam estado retornam void e deixam a mutação explícita na assinatura.

  3. 3

    Query = dado puro

    Métodos que devolvem valor não alteram estado observável — seguros para repetir e cachear.

  4. 4

    Separar na chamada

    Quem precisa agir e depois ler faz duas chamadas: depositar(100); saldo().

02Anatomia

Como o princípio escala

Camada 1/3Nível de método

Camada 1

Nível de método

cada assinatura declara: comando ou query

É o habitat original do CQS: um método muta e retorna void, ou retorna dado e não muta. A assinatura vira documentação confiável — o tipo de retorno denuncia a categoria sem abrir o corpo do método.

No código

depositar(valor: number): void  // comando
saldo(): number                 // query

Se violar: um 'sacar(): number' que muta e retorna cria chamadas com efeito invisível — repetir a linha num teste ou num retry debita duas vezes.

03Estrutura

Estrutura

Diagrama de classes
04Implementação

Código

class Conta {
  private _saldo = 0;

  // Comandos: mutam e retornam void
  depositar(valor: number): void { this._saldo += valor; }
  sacar(valor: number): void {
    if (valor > this._saldo) throw new Error("saldo insuficiente");
    this._saldo -= valor;
  }

  // Query: retorna dado, sem efeito colateral
  saldo(): number { return this._saldo; }
}

const c = new Conta();
c.depositar(100);      // comando
c.sacar(30);           // comando
console.log(c.saldo()); // query -> 70 (repetível e segura)
05No mundo real

Casos de uso

API previsível: GET sem efeito

Cenário

Uma equipe descobre que o endpoint GET /relatorios/mensal também marcava o relatório como 'visualizado' — e o prefetch do navegador estava marcando relatórios que ninguém abriu.

Como o padrão entra

CQS na fronteira HTTP: o GET vira query pura e a marcação vira um comando explícito (POST /relatorios/mensal/visualizacoes). Caches, proxies e prefetch voltam a funcionar sem efeitos fantasma.

Trade-off: O cliente agora faz duas chamadas onde fazia uma — o custo de uma ida extra à rede é o preço da leitura inofensiva.

Debugging sem medo

Cenário

Durante um incidente, o desenvolvedor precisa inspecionar o estado de um pedido em produção: chamar consultas no console, avaliar propriedades no debugger, logar valores em pontos do fluxo.

Como o padrão entra

Se as queries seguem CQS, inspecionar é gratuito: nenhuma consulta altera o pedido, então investigar não contamina a cena do crime. Watch expressions e logs podem chamar qualquer query à vontade.

Trade-off: A garantia só vale se TODAS as queries forem disciplinadas — uma única consulta com efeito escondido quebra a confiança no conjunto, e ninguém mais inspeciona tranquilo.

Caching de queries puras

Cenário

Um dashboard chama calcularIndicadores() dezenas de vezes por render, e o cálculo é caro. O time quer memoizar, mas tem medo do que a função faz por dentro.

Como o padrão entra

Sendo query pura (CQS), memoizar é seguro por construção: mesmo input, mesmo output, zero efeitos. Dá para cachear por argumento, invalidar por evento de comando, e até paralelizar chamadas.

Trade-off: Pureza é pré-condição, não mágica: a invalidação do cache continua sendo problema seu — comandos precisam sinalizar quando o dado subjacente mudou.

A query que debita

Compila, roda, passa no code review. E quebra depois.

O método retorna um número — cara de leitura — e por baixo muta o saldo. Quem chama 'só para inspecionar' altera o mundo; retry e log viram armadilha.

class Conta {
  private _saldo = 0;

  // Parece query (retorna number)... e debita.
  sacar(valor: number): number {
    if (valor > this._saldo) throw new Error("saldo insuficiente");
    this._saldo -= valor;
    return this._saldo; // <- comando disfarçado de leitura
  }
}

const c = new Conta();
c.sacar(0); // "so para ver o saldo" — em contas reais, o retry debita 2x
const a = c.sacar(10);
const b = c.sacar(10); // segunda chamada muda o mundo de novo
// Ninguem pode repetir, cachear ou logar sacar() sem medo.

O que quebra

  • No retryRepetir a chamada por timeout debita duas vezes — a leitura não era idempotente.
  • No debuggerWatch/evaluar a expressão muda o objeto sob inspeção.
  • No cacheMemoizar ou logar o retorno esconde que cada invocação tem efeito.

A saída: Comando sacar(valor): void e query saldo(): number. Se precisar do saldo novo, chame a query depois. Exceções atômicas (pop) são conscientes — não o padrão escondido.

07Cuidado

Armadilhas

  1. Aplicar como lei: o caso pop()

    pop() de uma pilha remove E retorna o elemento — viola CQS de propósito. Separar em peek() + remove() cria uma janela de corrida em contexto concorrente: outro thread pode alterar a pilha entre as duas chamadas. O mesmo vale para fetchAndIncrement de contadores atômicos e take() de filas. São exceções pragmáticas: a atomicidade é a semântica correta ali. CQS orienta; não tiraniza.

  2. Query com efeito escondido

    getUsuario() que atualiza 'ultimoAcesso', getter que dispara lazy-write, consulta que incrementa métrica de negócio: pareceram inofensivos no commit, mas quebram a garantia de repetibilidade. Nuance honesta: memoização e lazy-loading internos são aceitáveis, porque o estado OBSERVÁVEL não muda — o critério é se duas chamadas seguidas retornam o mesmo mundo.

  3. Comando que devolve dado 'por conveniência'

    salvar() que retorna a entidade inteira, setters encadeáveis que retornam this, criar() que devolve o objeto completo. Alguns são compromissos razoáveis (retornar o id gerado por um INSERT evita uma segunda ida ao banco), mas cada um dilui a convenção: o leitor deixa de confiar que 'retorna algo = não muta nada'. Se fizer a exceção, faça-a explícita e documentada.

08Decisão

Quando usar × quando evitar

Use quando

4
  • Ao projetar APIs e métodos de domínio, para deixar efeitos colaterais explícitos.
  • Quando quer que leituras sejam seguras para repetir, reordenar e cachear.
  • Para facilitar testes: queries puras não exigem verificar estado alterado.
  • Como base conceitual antes de adotar CQRS no nível arquitetural.

Evite quando

2
  • Operações naturalmente atômicas onde ler-e-mutar junto é a semântica correta (pop, fila concorrente, contadores atômicos).
  • Quando separar exigiria uma segunda ida ao recurso e o custo/condição de corrida não compensa.

Testar o que ficou

3 armadilhas de CQS (Command Query Separation) viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • GET × POST/PUT/DELETEO HTTP separa o verbo que só lê (GET, seguro e sem efeito) dos que mudam estado (POST, PUT, DELETE) — comando e query no nível do protocolo.
  • useState: [leitura, comando]O par que o hook devolve é a separação em pessoa: o primeiro elemento lê o estado, o segundo é o comando que o muda, nunca as duas coisas juntas.
  • dispatch × selector no ReduxSelectors leem a store sem tocá-la; o dispatch de uma action muda a store sem devolver dado — as duas metades do princípio, por design.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Comando muta; query só observa.
salvar(pedido);           // comando
const t = total(pedido);  // query

O que ele cobra

Nenhum salto a mais entre a chamada e o efeito.

  • Separar comando de query obriga a duas chamadas onde uma só resolveria (agir e depois ler)
  • Alguns casos são inerentemente 'pegar e remover', e a separação fica artificial

Não vale se: a operação é atomicamente 'mude e me diga o resultado', como um pop de fila — separar aí cria uma corrida entre as duas chamadas.

Costuma ser confundido com

Onde isto aparece

Continue por aqui