Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - Command (Comando) e Query (Consulta)

#command#dto#handler#query
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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