02 - Command (Comando) e Query (Consulta)
video: https://youtu.be/... no frontmatter da nota.Command (Comando) e Query (Consulta)
No CQRS, toda operação entra no sistema por um de dois tipos de mensagem. O comando expressa uma intenção imperativa, como AprovarPedido ou CancelarAssinatura, carrega os dados necessários para executá-la e pode ser recusado pelas regras de negócio. A consulta expressa uma pergunta, como ObterResumoDoPedido, carrega filtros e devolve um DTO pronto, sem alterar nada. Essa dicotomia, herdada do CQS, transforma a API do sistema em dois vocabulários distintos: um de verbos que mudam o mundo e outro de perguntas que o descrevem. Em produção, a disciplina de nomear, validar e tratar cada tipo corretamente é o que impede que a fronteira se degrade, e é sobre essa mecânica, do objeto de comando ao handler de consulta, que este arquivo se debruça.
Comando: intenção imperativa e validável
Um comando é um pedido de mudança que o sistema tem o direito de negar. Essa natureza define tudo nele: o nome no imperativo, porque ainda vai acontecer; a validação obrigatória, porque o estado atual pode proibir; e o retorno mínimo, porque a responsabilidade de calcular o novo estado é do sistema, não do chamador. O comando carrega dados de intenção, e não resultados; ele não sabe e não precisa saber qual evento será gerado.
Três características delimitam o objeto de comando. Primeiro, ele é um simples portador de dados, sem comportamento, geralmente imutável. Segundo, ele é direcionado a um agregado específico, identificado no payload, porque a decisão pertence àquele contexto. Terceiro, ele é único no tempo: executar o mesmo comando duas vezes não é idempotente por padrão, e quando a idempotência é exigida, ela é desenhada explicitamente, com chave de operação ou tratamento de estado já atingido.
A validação acontece em duas camadas com responsabilidades distintas. A sintática verifica forma, como campos obrigatórios, formatos e intervalos, e pode rodar antes de tocar o domínio. A semântica verifica regras contra o estado atual, como pedido pode ser pago, cliente está bloqueado, estoque existe, e pertence ao agregado. Confundir as camadas, colocando regras de negócio no controller ou validação de forma no agregado, espalha a lógica e enfraquece o modelo.
Transporte do comando: síncrono ou em fila
O mesmo comando pode ser entregue por dois transportes, e a escolha muda a experiência do usuário e o desenho dos erros. No transporte síncrono, o handler executa e responde na mesma requisição: recusas chegam imediatamente, o cliente sabe do resultado na hora, e a complexidade fica contida. No transporte em fila, o comando é aceito, enfileirado e processado depois: o sistema responde com aceitação e um identificador, e o resultado real aparece na leitura, minutos ou segundos depois.
A fila vale a pena quando a operação é demorada, quando picos precisam ser absorvidos ou quando a origem pode ficar indisponível, como comandos enviados por aplicativo móvel. O custo é dobrar a comunicação: o sistema confirma recebimento, e a confirmação do negócio chega pela leitura ou por notificação. Times que misturam os transportes sem critério costumam criar contratos confusos, com endpoints que às vezes confirmam o negócio e às vezes só o recebimento; a regra saudável é decidir por tipo de comando, documentar o contrato de cada um e nunca alterar o transporte de um comando sem revisar os consumidores.
Anatomia do command handler
O handler é o ponto único onde o comando é executado. O fluxo padrão é curto e repetível: receber o comando, carregar o agregado, delegar a decisão ao domínio, persistir e responder. Toda a inteligência fica no agregado; o handler é um condutor, e mantê-lo magro é o que preserva a testabilidade e a clareza do modelo.
// Comando: portador de dados da intenção, sem comportamento
public record AdicionarItemAoPedido(String pedidoId, String sku, int quantidade) {}
// Handler: recebe, valida sintaxe, delega a decisão e persiste
public class AdicionarItemAoPedidoHandler {
private final PedidoRepository repositorio;
public AdicionarItemAoPedidoHandler(PedidoRepository repositorio) {
this.repositorio = repositorio;
}
public void handle(AdicionarItemAoPedido comando) {
// validação sintática, antes de tocar no domínio
if (comando.quantidade() <= 0) {
throw new ArgumentoInvalido("quantidade deve ser positiva");
}
// carregamento do agregado com a versão atual do stream
Pedido pedido = repositorio.carregar(comando.pedidoId());
// decisão pertence ao domínio, não ao handler
pedido.adicionarItem(comando.sku(), comando.quantidade());
// persistência com concorrência otimista pela versão esperada
repositorio.salvar(pedido);
}
}
O handler não decide regra alguma; se um item não pode ser adicionado, é o Pedido que recusa, lançando a exceção de negócio. Essa divisão é o que permite trocar o mecanismo de transporte, de REST para fila, sem mudar uma linha de decisão. Também é o que permite testar o domínio sem infraestrutura, alimentando comandos e observando eventos.
Consulta: DTO puro sem lógica de negócio
A consulta tem a vida mais fácil e a disciplina mais simples de manter. Ela recebe parâmetros de filtro, executa uma leitura otimizada no read model e devolve um DTO moldado exatamente para o consumidor. Nenhuma regra de negócio mora ali; se a resposta precisa de um cálculo de negócio, o resultado já foi calculado no lado de comando e materializado na leitura.
// Consulta: parâmetros de filtro, sem comportamento
public record ObterResumoDoPedido(String pedidoId) {}
// Handler de consulta: leitura direta do read model, retorno pronto
public class ObterResumoDoPedidoHandler {
private final PedidosResumoDao dao;
public ObterResumoDoPedidoHandler(PedidosResumoDao dao) {
this.dao = dao;
}
public ResumoPedidoDto handle(ObterResumoDoPedido consulta) {
// nenhum cálculo, nenhuma regra; o read model já tem a resposta
return dao.buscarResumo(consulta.pedidoId());
}
}
A consequência prática é que consultas são baratas de evoluir. Adicionar um campo ao DTO, criar uma nova visão ou trocar o índice de busca não toca no modelo de domínio nem nas regras de escrita. Times com muitos relatórios e telas sentem essa liberdade no dia a dia: a demanda de leitura deixa de pressionar o coração do sistema e passa a ser atendida em uma camada projetada para isso.
A linha divisória entre consulta e comando tem um detalhe operacional frequentemente esquecido: custo e proteção. Consultas precisam ser baratas e tolerantes a falha parcial, porque servem a muitas pessoas ao mesmo tempo; por isso elas leem réplicas, aceitam cache e nunca travam o lado de escrita. Comandos são caros por natureza e protegidos, porque cada execução altera a verdade; por isso eles passam por validação completa, concorrência controlada e observabilidade detalhada. Aplicar ao comando a relaxação de uma consulta, ou a uma consulta o rigor de um comando, é o erro de custo mais comum na fronteira.
Convenções de nomenclatura
Nomes são a documentação diária da fronteira. Times maduros padronizam a gramática e a revisam em code review, porque um nome fora do padrão sinaliza quase sempre um desenho fora do lugar.
| Tipo | Padrão de nome | Exemplos | Retorno típico |
|---|---|---|---|
| Comando | Verbo no imperativo com objeto de negócio | CriarPedido, AprovarPedido, BloquearConta | Confirmação, versão ou nada |
| Consulta | Prefixo de leitura com substantivo | ObterPedido, ListarPedidosAbertos, BuscarProdutos | DTO ou página de DTOs |
| Evento resultante | Fato no passado | PedidoCriado, ContaBloqueada | Consumido por projeções |
| DTO de leitura | Substantivo com forma do consumidor | ResumoPedidoDto, ExtratoContaDto | Corpo da resposta |
A coesão entre os nomes conta uma história auditável: o comando AprovarPedido gera o evento PedidoAprovado, que alimenta a visão lida pela consulta ObterPedidosPendentesAprovacao. Quando os três lados do ciclo falam a mesma língua, rastrear um comportamento do sistema vira leitura, e não investigação.
O grau de formalidade dos objetos varia com a stack, mas o conteúdo não. Em Java e C#, records e structs são a escolha natural para comandos e DTOs, imutáveis e baratos de testar. Em Go, structs com campos exportados cumprem o papel. Em TypeScript, interfaces com Readonly arrancam a mutação acidental. O que importa é a propriedade: nenhum objeto de comando ou consulta deve carregar comportamento, e nenhum handler deve mutar o objeto que recebeu, porque a mensagem é o registro da intenção, e registros não se alteram.
Operações que tensionam a dicotomia
Algumas situações reais não se encaixam perfeitamente, e a resposta de desenho para cada uma evita adaptações erradas. O comando que precisa devolver um identificador, como CriarPedido que retorna o número do pedido, é aceitável e comum: o retorno serve para referenciar o trabalho, não para entregar estado calculado. O login, que verifica credenciais e registra sessão, decompõe-se em consulta de verificação seguida de comando de registro de sessão. A busca com autocompletar é consulta pura, e o log de uso dela, quando necessário, é um comando separado e assíncrono.
A regra geral que resolve os casos duvidosos: se a operação muda algo que o sistema considera verdade, ela é comando; se apenas responde, é consulta; se faz as duas coisas, é decomposta. A exceção aceita, com justificativa registrada, é o retorno de identificador no comando. Fora disso, misturas indicam que a operação precisa ser repensada, e não que o padrão precisa de ajuste.
Um caso especial recorrente é a importação em lote, como carregar dez mil itens de um catálogo. Tratar cada linha como comando sincrono quebra o orçamento de latência; tratar como um único comando gigante cria uma transação inviável. A resposta comum é um comando de início de importação, com referência ao arquivo, seguido de processamento assíncrono que emite eventos por lote e uma consulta de status da importação. O desenho respeita a dicotomia e escala, e é um bom exemplo de como os casos difíceis se resolvem decompondo, e não relaxando o padrão.
Erros comuns e boas práticas
- Comandos genéricos, como AtualizarCliente, que escondem a intenção real; prefira comandos específicos, como AlterarEndereco, que deixam claro o efeito e facilitam a trilha de auditoria.
- Handlers de consulta com lógica de negócio, como calcular status por regra na hora da leitura; o cálculo pertence ao comando e deve ser materializado na escrita.
- Reutilizar o DTO de leitura como comando; os dois têm ciclos de vida e validações diferentes, e o acoplamento quebra os dois lados ao mesmo tempo.
- Retornar a entidade do agregado no comando, expondo o modelo interno e criando dependência de formato; retorne confirmação mínima e deixe a leitura montar a visão.
- Executar comandos em paralelo sem concorrência explícita; sem versão esperada, escritas simultâneas perdem dados em silêncio.
- Nomear consulta com verbo de mudança, como ProcessarRelatorio, quando o trabalho real é on-demand e tem efeito; nesse caso, é um comando, e deve ser tratado como tal.
- Perder o contexto entre os serviços: comando sem correlationId e causationId deixa a trilha incompleta, e o suporte não consegue amarrar requisição, comando e eventos gerados.
Práticas que consolidam a fronteira: valide sintaxe na borda e semântica no agregado, com erros distintos e códigos claros; mantenha comandos pequenos e um handler por comando, evitando handlers gigantes que acumulam casos; registre correlationId no comando e leve-o até os eventos, amarrando a trilha de ponta a ponta; e versionize DTOs publicados, porque a leitura é contrato tanto quanto a escrita.
| Sintoma em produção | Causa provável | Mitigação |
|---|---|---|
| Comandos com regra de negócio copiada do domínio | Decisão vazando para o handler | Migrar a regra para o agregado e testar no domínio |
| Leitura lenta com cálculo em tempo real | Regra calculada no handler de consulta | Materializar o resultado na escrita e ler pronto |
| Perda de atualização sob concorrência | Comando sem versão esperada | Adotar compare-and-set e retry com estado novo |
| API instável a cada mudança de tela | DTO de leitura compartilhado entre consumidores | Criar DTO por consumidor ou por tipo de consulta |