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)

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:

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:

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.

public class CriarPedidoCommand 
{
    public Guid ClienteId { get; set; }
    public List<Guid> Itens { get; set; }
}
public class CriarPedidoHandler {
    public void Handle(CriarPedidoCommand command)
    {
        var pedido = new Pedido(command.ClienteId, command.Itens);
        pedido.Validar();
        _repo.Salvar(pedido);
    }
}

Lado da Leitura (Query Side)

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

public class GetPedidosPorClienteQuery
{
    public Guid ClienteId { get; set; }
}
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));
    }
}

Quando aplicar CQRS

✅ Indicado para:

❌ Evite em:

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]

Estrutura típica com MediatR

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

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);
    }
}

1. Command

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

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);
    }
}

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

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);
    }
}

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.

MediatR