Macoratti 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.

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.

2. Queries (Consultas)

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.

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:

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:

  1. A camada de aplicação recebe o comando.
  2. A aplicação realiza as validações necessárias.
  3. Quando existem regras de domínio, o agregado correspondente protege suas invariantes.
  4. A aplicação coordena a persistência das alterações.
  5. 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:

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, retornando DTOs específicos para cada necessidade.

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:

Isso pode gerar problemas:

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ódulo

Lado 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:

  1. Recebemos a intenção de confirmar o pedido.
  2. Recuperamos o agregado do banco de dados.
  3. O agregado executa a operação e protege suas regras.
  4. 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:

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:

Por outro lado, CQRS pode não ser a melhor escolha quando:

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.

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

Macoratti

Referências:


José Carlos Macoratti