Commands e Queries não Devem Ter a Mesma Responsabilidade

- Leitura
- 12 min
- Código
- 10
Este artigo discute por que operações de escrita devem proteger as regras de negócio enquanto operações de leitura devem recuperar e apresentar dados de maneira eficiente. (Gerado com ajuda da IA↳Inteligência artificial440 conteúdosUX e IA: Transformando Experiências Digitais com Inteligência ArtificialProduto & UX · jan 2025MCP: O que é e por que você vai ouvir falar disso em breve?AI · jul 2025IA generativa e a urgência de reconstruir nossa relação com a verdadeAI · jun 2025Ver tudo em AI →, ajustado e revisado por Macoratti)
Isso parece óbvio à primeira vista. Mas muitos sistemas são construídos como se operações de leitura e escrita fossem exatamente iguais.
Um único serviço acaba acumulando responsabilidades distintas: coordenar operações de escrita, proteger regras de negócio, executar consultas, montar projeções, aplicar filtros, ordenações e preparar dados para apresentação.
Com o tempo, esse serviço se torna inchado, difícil de testar e complexo de evoluir.
Imagine um PedidoService responsável por criar e cancelar pedidos, confirmar pagamentos, consultar pedidos por identificador, pesquisar pedidos por período, calcular totais de vendas, montar dados para dashboards e preparar informações para diferentes telas.
Embora todas essas operações estejam relacionadas ao mesmo contexto de negócio, elas possuem necessidades arquiteturais diferentes.
É exatamente por isso que a separação entre Commands e Queries é tão relevante. O objetivo não é complicar a arquitetura, mas tornar as responsabilidades mais claras.
1. O princípio por trás da separação: CQS
Antes de falar sobre CQRS, precisamos conhecer um princípio importante chamado CQS (Command Query Separation), introduzido por Bertrand Meyer.
A ideia fundamental é simples: uma operação deve executar uma consulta ou produzir uma alteração no estado do sistema, mas não deve fazer as duas coisas simultaneamente.
CQS estabelece duas categorias de operações:
1. Commands (Comandos)
São operações que modificam o estado do sistema, mas não retornam informações de consulta como resultado da operação.
- Confirmar um pedido;
- Cancelar uma consulta;
- Atualizar um cadastro;
- Efetuar um pagamento.
2. Queries (Consultas)
São operações que retornam informações, mas não modificam o estado observável do sistema.
- Obter um pedido por identificador;
- Listar médicos disponíveis;
- Pesquisar atendimentos;
- Consultar o histórico de pagamentos.
A ideia central é separar claramente quem modifica o estado de quem apenas consulta informações. Essa separação reduz efeitos colaterais inesperados e torna o comportamento das operações mais previsível.
2. O que é um Command (Comando)?
Um Command representa uma intenção de modificar o estado do sistema. Ele expressa uma solicitação para executar uma ação de negócio.
public record AgendarConsultaCommand(
Guid PacienteId,
Guid MedicoId,
DateTime DataHora);
Outros exemplos:
CancelarConsultaCommandConcluirConsultaCommandConfirmarPagamentoCommandCriarPedidoCommand
Observe que um comando representa uma intenção, e não simplesmente uma atualização de propriedade.
Por exemplo, é preferível utilizar ConfirmarPedidoCommand em vez de um comando genérico como AtualizarStatusPedidoCommand quando a confirmação representa uma operação de negócio com regras próprias.
O processamento de um Command normalmente envolve:
- A camada de aplicação recebe o comando.
- A aplicação realiza as validações necessárias.
- Quando existem regras de domínio, o agregado correspondente protege suas invariantes.
- A aplicação coordena a persistência das alterações.
- Eventos de domínio podem ser registrados quando ocorrerem fatos relevantes para o negócio.
O ponto importante é que Commands devem respeitar as regras de negócio e preservar a consistência do domínio.
A camada de aplicação coordena a operação, enquanto o domínio protege as regras que não podem ser violadas.
3. O que é uma Query (Consulta)?
Uma Query representa uma solicitação para recuperar informações sem modificar o estado de negócio do sistema.
Exemplos:
ObterConsultaPorIdQueryObterDisponibilidadeMedicoQueryPesquisarConsultasQueryObterDashboardReceitaQuery
Uma Query tem uma responsabilidade bem definida: recuperar e entregar os dados solicitados.
E aqui está um ponto importante: uma Query não precisa necessariamente passar pelo modelo de domínio completo.
Imagine que uma interface precisa exibir nome do médico, nome do paciente, data e horário da consulta, status do pagamento e informações adicionais para apresentação.
Forçar essa leitura através de um grafo complexo de agregados pode ser desnecessário.
Para leituras, podemos utilizar projeções diretamente do banco de dados↳Banco de dados134 conteúdosSQL ou NoSQL: eis a questão!!Data · mar 2020Banco de dados: como organizar e dar segurança para milhões de dados de loteriasData · mai 20215 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data →, retornando DTOs específicos para cada necessidade.
- Dapper para executar consultas SQL diretamente;
- EF Core utilizando projeções com
Select; - EF Core utilizando AsNoTracking
()para consultas que não precisam rastrear alterações.
A Query pode realizar cálculos complexos para produzir informações, como totais financeiros, indicadores e métricas. O que ela não deve fazer é modificar o estado de negócio ou assumir a responsabilidade de autorizar alterações.
Uma observação: operações auxiliares de infraestrutura, como telemetria e logs, podem ocorrer durante uma consulta. Isso não significa que a Query esteja modificando o estado de negócio. Entretanto, a operação de consulta não deve produzir efeitos colaterais observáveis que contrariem o princípio CQS.4. CQS e CQRS: qual é a diferença?
Embora estejam relacionados, CQS e CQRS não são a mesma coisa.
CQS (Command Query Separation) é um princípio de projeto que separa operações de consulta e operações de alteração.
CQRS (Command Query Responsibility Segregation) amplia essa ideia para a arquitetura da aplicação, permitindo utilizar modelos distintos para escrita e leitura.
No CQRS, não precisamos utilizar a mesma representação dos dados para executar operações de escrita e atender consultas.
Imagine um sistema de pedidos. Para confirmar um pedido, precisamos trabalhar com o modelo de domínio, respeitar suas regras e garantir que a operação seja válida. Entretanto, para exibir uma lista de pedidos em uma tela, precisamos apenas dos dados necessários para apresentação.
O modelo de escrita e o modelo de leitura podem, portanto, ter estruturas diferentes.
Essa é a essência do CQRS.
5. Por que misturar Commands e Queries gera problemas?
Muitos sistemas começam com um único serviço por entidade, como AgendaService, PedidoService ou ClienteService.
Inicialmente, essa abordagem parece simples. Mas, conforme o sistema cresce, esses serviços passam a acumular responsabilidades:
- Criar, alterar e cancelar registros;
- Executar consultas e pesquisas;
- Aplicar regras de negócio;
- Calcular indicadores;
- Montar relatórios;
- Preparar DTOs para diferentes interfaces.
Isso pode gerar problemas:
- A lógica de escrita fica entrelaçada com a lógica de leitura;
- Regras de negócio se misturam com necessidades de apresentação;
- O ajuste de desempenho das consultas fica mais difícil;
- Alterações em uma responsabilidade podem afetar operações não relacionadas;
- Os testes ficam mais difíceis de organizar e manter.
A separação entre Commands e Queries ajuda a reduzir esse acoplamento.
6. Entendendo o CQRS sem misticismo
CQRS não exige obrigatoriamente bancos de dados separados, Event Sourcing ou infraestruturas complexas.
Podemos aplicar CQRS em um monolito modular, utilizando a mesma aplicação e o mesmo banco de dados, mas mantendo responsabilidades e modelos distintos.
Estrutura de CQRS prático em um móduloLado de Escrita (Write Side)
- Commands;
- Command Handlers;
- Agregados;
- Repositórios;
- Regras e invariantes de domínio;
- Eventos de domínio, quando necessários.
Foco: proteger regras de negócio, garantir consistência e executar alterações válidas.
Lado de Leitura (Read Side)
- Queries;
- Query Handlers;
- Projeções SQL;
- Dapper ou EF Core;
- DTOs específicos para cada necessidade de leitura.
Foco: recuperar informações de maneira eficiente e entregar os dados no formato adequado ao consumidor.
A estrutura pode ser representada assim:
Observe que os dois lados podem compartilhar o mesmo banco de dados. O que está sendo separado são os modelos e as responsabilidades, não necessariamente a infraestrutura de persistência.
Diferentes níveis de implementação
Podemos aplicar CQRS em diferentes níveis de complexidade:
1. Separação lógica
Commands e Queries possuem handlers e responsabilidades diferentes, mas utilizam o mesmo banco de dados. É uma abordagem bastante adequada para monolitos modulares.
2. Modelos de dados separados
O modelo de escrita utiliza uma estrutura orientada às regras de negócio, enquanto o modelo de leitura utiliza projeções específicas para atender às consultas. Os dois modelos ainda podem compartilhar o mesmo banco.
3. Armazenamentos de dados separados
O lado de escrita utiliza um banco de dados e o lado de leitura utiliza outro armazenamento, possivelmente com uma tecnologia diferente.
Nesse cenário, normalmente precisamos de mecanismos para sincronizar os modelos, frequentemente utilizando eventos e processamento assíncrono.
Uma consequência importante é a possibilidade de consistência eventual. Isso significa que uma alteração pode ter sido confirmada no lado de escrita, mas ainda não estar refletida no modelo de leitura.
Para aplicações que precisam dessa arquitetura mais avançada, é necessário considerar mecanismos como Transactional Outbox, processamento idempotente e estratégias de recuperação de falhas.
7. Exemplo prático: Commands e Queries com .NET e EF Core
Vamos demonstrar como aplicar a separação em uma aplicação simples de pedidos.
O objetivo é utilizar o mesmo DbContext, mas tratar escrita e leitura de maneiras diferentes.
7.1. Modelo de domínio
Primeiro, temos a entidade Pedido, que protege suas regras de negócio.
public class Pedido
{
public Guid Id { get; private set; }
public string Cliente { get; private set; } = string.Empty;
public decimal ValorTotal { get; private set; }
public bool Confirmado { get; private set; }
public void Confirmar()
{
if (Confirmado)
throw new InvalidOperationException(
"O pedido já foi confirmado.");
if (ValorTotal <= 0)
throw new InvalidOperationException(
"O pedido não possui valor válido.");
Confirmado = true;
}
}
Observe que a regra de confirmação está na própria entidade. Não estamos deixando a responsabilidade de proteger essa regra para o Handler ou para a interface.
7.2. Lado de escrita: Command
Primeiro, definimos o comando:
public record ConfirmarPedidoCommand(Guid PedidoId);
Ele representa a intenção de confirmar um pedido.
Agora, implementamos seu Handler:
public class ConfirmarPedidoHandler(AppDbContext context)
{
public async Task HandleAsync(
ConfirmarPedidoCommand command,
CancellationToken cancellationToken)
{
var pedido = await context.Pedidos
.FirstOrDefaultAsync(
p => p.Id == command.PedidoId,
cancellationToken);
if (pedido is null)
throw new InvalidOperationException(
"Pedido não encontrado.");
pedido.Confirmar();
await context.SaveChangesAsync(cancellationToken);
}
}
O fluxo é simples:
- Recebemos a intenção de confirmar o pedido.
- Recuperamos o agregado do banco de dados.
- O agregado executa a operação e protege suas regras.
- Persistimos a alteração.
O Handler coordena a operação, mas não é o responsável por implementar as regras de negócio.
7.3. Lado de leitura: Query
Agora imagine que precisamos apresentar uma lista de pedidos em uma tela. Não precisamos carregar o agregado completo para isso.
Primeiro, criamos um DTO específico para leitura:
public record PedidoDto(
Guid Id,
string Cliente,
decimal ValorTotal,
bool Confirmado);
Definimos a Query:
public record ObterPedidosQuery();
E implementamos seu Handler:
public class ObterPedidosHandler(AppDbContext context)
{
public async Task<List<PedidoDto>> HandleAsync(
ObterPedidosQuery query,
CancellationToken cancellationToken)
{
return await context.Pedidos
.AsNoTracking()
.Select(p => new PedidoDto(
p.Id,
p.Cliente,
p.ValorTotal,
p.Confirmado))
.ToListAsync(cancellationToken);
}
}
Observe as diferenças:
- Não carregamos agregados para realizar a leitura;
- Não precisamos executar comportamentos do domínio;
- Não precisamos utilizar o mesmo fluxo de acesso utilizado na escrita;
- Projetamos diretamente os dados necessários para a interface;
- Utilizamos
AsNoTracking()porque não pretendemos modificar as entidades recuperadas.
A consulta retorna somente as informações necessárias, evitando o trabalho adicional de rastreamento de alterações do EF Core.
7.4. O que esse exemplo demonstra?
Temos dois fluxos independentes em termos de responsabilidade:
Command
Query
Ambos utilizam o mesmo banco de dados, mas possuem objetivos e formas de trabalhar diferentes.
Esse é um exemplo de CQRS simplificado, sem a necessidade de bancos separados, mensageria ou Event Sourcing.
Naturalmente, em uma aplicação real, também devemos tratar autorização, validação de entrada, concorrência, transações e tratamento adequado de erros. O exemplo está propositalmente reduzido para destacar a separação arquitetural.
8. Erros comuns ao aplicar CQRS
Mesmo sendo um padrão relativamente simples de entender, existem alguns erros frequentes na sua aplicação.
1. Reutilizar entidades de domínio diretamente para telas
Isso pode fazer com que necessidades de apresentação contaminem o modelo de domínio. Prefira DTOs de leitura específicos para cada necessidade.
2. Colocar regras críticas nos Query Handlers
Queries podem realizar cálculos complexos e produzir indicadores de negócio. Entretanto, não devem ser responsáveis por autorizar alterações ou proteger invariantes.
Por exemplo, uma Query pode verificar e apresentar os horários aparentemente disponíveis de um médico. Mas o Command responsável por agendar uma consulta precisa verificar novamente a disponibilidade e garantir que a operação respeite as regras de negócio, inclusive diante de operações concorrentes.
3. Criar Commands genéricos demais
Um comando deve expressar uma intenção de negócio. ConfirmarPedidoCommand é mais expressivo do que um comando genérico para alterar propriedades de um pedido.
4. Acreditar que CQRS exige bancos separados
A separação dos modelos pode acontecer dentro da mesma aplicação e utilizando o mesmo banco de dados. Bancos separados são uma possibilidade de implementação mais avançada, não um requisito obrigatório.
5. Confundir CQRS com Event Sourcing
São padrões diferentes e podem ser utilizados em conjunto, mas um não exige o outro. CQRS separa os modelos de leitura e escrita. O Event Sourcing utiliza uma sequência de eventos como fonte de verdade para reconstruir o estado do sistema.
6. Criar abstrações desnecessárias
Não precisamos criar dezenas de interfaces, repositórios e serviços apenas para dizer que estamos utilizando CQRS. A arquitetura deve resolver problemas reais e manter o código compreensível.
9. Quando utilizar e quando evitar CQRS?
CQRS pode ser particularmente interessante quando:
- O domínio possui regras de negócio complexas;
- As operações de escrita possuem invariantes importantes;
- As necessidades de leitura são muito diferentes das necessidades de escrita;
- Existem consultas complexas e projeções específicas;
- O desempenho das leituras precisa ser otimizado independentemente;
- O sistema possui diferentes necessidades de evolução para leitura e escrita.
Por outro lado, CQRS pode não ser a melhor escolha quando:
- O domínio é simples;
- A aplicação possui operações CRUD convencionais;
- As leituras e escritas possuem necessidades semelhantes;
- A separação não proporciona benefícios concretos;
- O custo de manutenção das abstrações supera os benefícios obtidos.
Para aplicações simples, uma abordagem CRUD tradicional pode ser perfeitamente adequada.
Não adote CQRS apenas porque ele representa uma arquitetura mais sofisticada. Utilize-o quando a separação realmente trouxer benefícios para o sistema.Conclusão
Commands e Queries podem atuar sobre a mesma área de negócio, mas possuem responsabilidades diferentes.
Commands representam intenções de alteração e devem garantir que as regras de negócio e as invariantes sejam respeitadas.
Queries representam solicitações de leitura e devem recuperar informações de maneira eficiente, utilizando projeções e estruturas adequadas às necessidades dos consumidores.
O CQRS permite separar os modelos utilizados para essas duas finalidades, sem exigir necessariamente bancos de dados distintos, mensageria ou Event Sourcing.
Quando aplicamos essa separação de maneira consciente, conseguimos reduzir o acoplamento, melhorar a organização do código e facilitar a evolução da aplicação.
Mas existe uma recomendação importante: a melhor arquitetura não é necessariamente a que possui mais padrões e abstrações, mas aquela que resolve os problemas do sistema com a menor complexidade possível.
















