Commands e Queries não Devem Ter a Mesma Responsabilidade
|
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, 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.
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:
São operações que modificam o estado do sistema, mas não retornam informações de consulta como resultado da operação.
São operações que retornam informações, mas não modificam o estado observável do sistema.
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.
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:
CancelarConsultaCommandConcluirConsultaCommandConfirmarPagamentoCommandCriarPedidoCommandObserve 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:
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.
Uma Query representa uma solicitação para recuperar informações sem modificar o estado de negócio do sistema.
Exemplos:
ObterConsultaPorIdQueryObterDisponibilidadeMedicoQueryPesquisarConsultasQueryObterDashboardReceitaQueryUma 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, retornando DTOs específicos para cada necessidade.
Select;() 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.
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.
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:
Isso pode gerar problemas:
A separação entre Commands e Queries ajuda a reduzir esse acoplamento.
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.
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.
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.
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.
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.
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:
O Handler coordena a operação, mas não é o responsável por implementar as regras de negócio.
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:
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.
Temos dois fluxos independentes em termos de responsabilidade:
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.
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.
CQRS pode ser particularmente interessante quando:
Por outro lado, CQRS pode não ser a melhor escolha quando:
Para aplicações simples, uma abordagem CRUD tradicional pode ser perfeitamente adequada.
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.
E estamos conversados! ![]()
"(Disse Jesus) Hipócritas, bem profetizou Isaías a vosso
respeito, dizendo: Este povo se aproxima de mim com a sua boca e me honra com os
seus lábios, mas o seu coração está longe de mim.Mas, em vão me adoram,
ensinando doutrinas que são preceitos dos homens."
Mateus 15:7-9
Referências: