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.
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.
- Chamador
- ordena (void)Comando depositar()
- mutaEstado muda
- depois observaQuery saldo()
- retornaDado (sem efeito)
- 1
Classificar a intenção
A operação pergunta (ler) ou manda (mutar)? Escolha um dos dois papéis.
- 2
Comando = void
Métodos que mudam estado retornam void e deixam a mutação explícita na assinatura.
- 3
Query = dado puro
Métodos que devolvem valor não alteram estado observável — seguros para repetir e cachear.
- 4
Separar na chamada
Quem precisa agir e depois ler faz duas chamadas: depositar(100); saldo().
Como o princípio escala
Camada 1/3Nível de método
use ← →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 // querySe 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.
Estrutura
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)class Conta:
def __init__(self):
self._saldo = 0
# Comandos: mutam e retornam None
def depositar(self, valor: int) -> None:
self._saldo += valor
def sacar(self, valor: int) -> None:
if valor > self._saldo:
raise ValueError("saldo insuficiente")
self._saldo -= valor
# Query: retorna dado, sem efeito colateral
@property
def saldo(self) -> int:
return self._saldo
c = Conta()
c.depositar(100) # comando
c.sacar(30) # comando
print(c.saldo) # query -> 70 (repetível e segura)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.
Armadilhas
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.
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.
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.
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); // queryO 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.