DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

CQRS (Command Query Responsibility Segregation)

Introdução

CQRS singifica Command Query Responsibility Segragation. O objetivo principal é separar as operações de leitura (Queries) das operações de escritas (Commands) em modelos distintos. (Em APIs, essa separação costuma se refletir em endpoints distintos, mas não é obrigatório)

  • Leitura (Query): consultar dados, mas não altera o estado
  • Escrita (Command): criar, atualizar e deletar

Em sistemas tradicionais (CRUD padrão), usamos a mesma classe, entidade e até estrutura de banco, tanto pra salvar quanto pra consultar dados. É simples no começo, mas com o tempo gera acoplamento e limitações.

Conflitos de objetivos

Antes de entendermos a resolução de problema proposto pelo CQRS é fundamental compreeendermos a diferença entre leitura e escrita aplicados a contextos de sistemas:

  • O modelo de escrita precisa representar regras de negócios e manter consistência. Exemplo: não permitir criar um pedido sem cliente, sem itens, etc.
  • O modelo de leitura, por sua vez, precisa ser flexível e rápido para exibir as informações. Exemplo: mostrar o nome do cliente, total do pedido e status em uma única tela

Se você utilizar os mesmos modelos, como proposto pelos sistemas tradicionais (CRUD), ele tentará servir dois propósitos ao mesmo tempo, o que deixa pesado e confuso.

Motivação e origem do CQRS

O CQRS surge da evolução do CQS (Command Query Separation). Que diz que: “Um método deve ou não alterar o estado do sistema (Command) ou retornar um valor (Query), mas nunca fazer as duas coisas”, portanto, o CQRS leva a ideia além, não apenas a nível de método, mas a nível de arquitetura.

Os principais benefícios do CQRS são:

  • Separação clara das responsabilidades Cada parte do código tem um propósito único. Isso torna o código mais legível, testável e modular
  • Otimização independente Você pode otimizar escrita para consistência e leitura para perfomance — até em bancos diferentes
  • Escalabilidade As leituras e escritas podem escalar separadamente
  • Combinação com Event Sourcing O CQRS é a base ideal para implementar Event Sourcing, onde o estado do sistema é reconstruído a partir de eventos e a leitura é derivada desses eventos

Estrutura conceitual do CQRS

Como já vimos anteriormente, em uma aplicação CQRS temos dois lados bem definidos. Lado da escrita (Command) e Lado da Leitura (Query).

Lado da Escrita (Command Side)

Responsável por alterar o estado do sistema. Aqui moram as regras de negócios, as validações e o modelo de domínio.

  • Command: uma intenção de ação (ex: CriarPedidoCommand)
  • Command Handle: executa o comando (valida, aplica a lógica e grava no banco)
  • Domain Model: representa entidades, agradados e invariantes
public class CriarPedidoCommand 
{
    public Guid ClienteId { get; set; }
    public List<Guid> Itens { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class CriarPedidoHandler {
    public void Handle(CriarPedidoCommand command)
    {
        var pedido = new Pedido(command.ClienteId, command.Itens);
        pedido.Validar();
        _repo.Salvar(pedido);
    }
}
Enter fullscreen mode Exit fullscreen mode

Lado da Leitura (Query Side)

Responsável por consultar os dado sem alterar o estado. Aqui o objetivo é simplicidade, rapidez e perfomance

  • Query: representa uma requisição de dados (ex: GetPedidosPorClienteQuery)
  • Query Handler: executa a busca (com Dapper, SQL, cache, etc).
  • Read Model: DTOs otimizados para exibição (sem regas de negócio)
public class GetPedidosPorClienteQuery
{
    public Guid ClienteId { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class GetPedidosPorClienteHandler
{
    public IEnumerable<PedidoResumoDto> Handle(GetPedidosPorClienteQuery query)
    {
        return _context.Pedidos
            .Where(p => p.ClienteId == query.ClienteId)
            .Select(p => new PedidoResumoDto(p.Id, p.Total, p.Status));
    }
}
Enter fullscreen mode Exit fullscreen mode

Quando aplicar CQRS

✅ Indicado para:

  • Sistemas de alta complexidade de domínio.
  • Cenários com muito mais leitura que escrita.
  • Regras de negócio densas.
  • Escalabilidade ou Event Sourcing.
  • Integração com bancos diferentes (SQL + NoSQL).

❌ Evite em:

  • CRUDs simples ou pequenos.
  • MVPs, protótipos, microprojetos.
  • Times ainda sem maturidade arquitetural.

CQRS + Mediator Pattern

Na prática, em .NET o CQRS é quase sempre utilizado com o padrão Mediator. No .NET moderno, esse padrão é muito utilizado através da biblioteca MediatR.

O papel do Mediator no CQRS

Como já sabemos, no CQRS separamos comandos (escritas) e consultas (leituras)… Mas quem orquestra essas operações? É nesse momento que entra o Mediator. Ele age como ponto central por onde os commands e queries passam. Desse modo, os controladores (controllers) não precisam conhecer diretamente os handlers.

Como funciona o fluxo?

  1. O Controller recebe uma requisição HTTP
  2. Ele cria um Command ou Query
  3. Ele envia isso para o Mediator (IMediator).
  4. O Mediator localiza o Handler correspondente e o executa
[Cliente / Front-end]
        |
        | HTTP POST /api/pedidos
        v
[PedidoController] 
        | Recebe CriarPedidoCommand
        v
[Mediator (IMediator)]
        | Localiza o handler correto (CriarPedidoHandler)
        v
[CriarPedidoHandler] 
        | Executa lógica de criação do pedido
        | (gera Guid, salva no banco, etc.)
        v
[Mediator] 
        | Retorna resultado (Guid do pedido)
        v
[PedidoController] 
        | Retorna Ok(Guid) para o cliente
        v
[Cliente / Front-end]

Enter fullscreen mode Exit fullscreen mode

Estrutura típica com MediatR

Application/
 ├── Commands/
     ├── CriarPedido/
          ├── CriarPedidoCommand.cs
          └── CriarPedidoHandler.cs
 ├── Queries/
     ├── GetPedidosPorCliente/
          ├── GetPedidosPorClienteQuery.cs
          └── GetPedidosPorClienteHandler.cs
 └── Behaviors/ (opcional: logs, validação etc.)
Enter fullscreen mode Exit fullscreen mode

Exemplo real com MediatR

A seguir, irei apresentar um bloco de código contendo exemplo real do mediator e logo após sua aplicação em detalhes.

// Command
public record CriarPedidoCommand(Guid ClienteId, decimal ValorTotal) : IRequest<Guid>;

// Handler
public class CriarPedidoHandler : IRequestHandler<CriarPedidoCommand, Guid>
{
    public Task<Guid> Handle(CriarPedidoCommand request, CancellationToken cancellationToken)
    {
        var pedidoId = Guid.NewGuid();
        Console.WriteLine($"Pedido criado para cliente {request.ClienteId} com valor {request.ValorTotal}");
        return Task.FromResult(pedidoId);
    }
}

// Controller
[ApiController]
[Route("api/pedidos")]
public class PedidoController : ControllerBase
{
    private readonly IMediator _mediator;

    public PedidoController(IMediator mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> CriarPedido([FromBody] CriarPedidoCommand command)
    {
        var id = await _mediator.Send(command);
        return Ok(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

1. Command

public record CriarPedidoCommand(Guid ClienteId, decimal ValorTotal) : IRequest<Guid>;
Enter fullscreen mode Exit fullscreen mode
  • record é uma forma imutável de criar um tipo simples no C#.
  • IRequest<Guid> vem do MediatR e indica que esse comando espera como resposta um Guid (o ID do novo pedido).
  • Nenhuma lógica aqui — é só dados.

2. Handler

public class CriarPedidoHandler : IRequestHandler<CriarPedidoCommand, Guid>
{
    public Task<Guid> Handle(CriarPedidoCommand request, CancellationToken cancellationToken)
    {
        var pedidoId = Guid.NewGuid();
        Console.WriteLine($"Pedido criado para cliente {request.ClienteId} com valor {request.ValorTotal}");
        return Task.FromResult(pedidoId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Esse é o módulo que realmente executa o comando.

  • Implementa IRequestHandler<TRequest, TResponse>. No caso, lida com CriarPedidoCommand e retorna um Guid.
  • O método Handle é o ponto onde a ação acontece:
    • Recebe o comando (request), que traz ClienteId e ValorTotal.
    • Cria um novo pedidoId.
    • Faz alguma ação (aqui só imprime, mas poderia salvar no banco).
    • Retorna o ID do pedido.

Ou seja: o handler é o executador da intenção.

3. Controller

[ApiController]
[Route("api/pedidos")]
public class PedidoController : ControllerBase
{
    private readonly IMediator _mediator;

    public PedidoController(IMediator mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> CriarPedido([FromBody] CriarPedidoCommand command)
    {
        var id = await _mediator.Send(command);
        return Ok(id);
    }
}

Enter fullscreen mode Exit fullscreen mode

O controller recebe a requisição HTTP (ex: via Postman ou front-end).

  1. Recebe um CriarPedidoCommand no corpo da requisição.
  2. Chama _mediator.Send(command), o Mediator encontra o handler certo e executa.
  3. Recebe o resultado (id) e devolve no Ok(id).

O controller não sabe qual handler executa o comando, só fala com o Mediator. Isso mantém o baixo acoplamento e deixa a aplicação mais organizada.

Top comments (0)