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; }
}
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
-
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; }
}
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:
- 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?
- O Controller recebe uma requisição HTTP
- Ele cria um Command ou Query
- Ele envia isso para o Mediator (
IMediator
). - 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>;
-
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 umGuid
(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);
}
}
Esse é o módulo que realmente executa o comando.
- Implementa
IRequestHandler<TRequest, TResponse>
. No caso, lida comCriarPedidoCommand
e retorna umGuid
. - O método
Handle
é o ponto onde a ação acontece:- Recebe o comando (
request
), que trazClienteId
eValorTotal
. - Cria um novo
pedidoId
. - Faz alguma ação (aqui só imprime, mas poderia salvar no banco).
- Retorna o ID do pedido.
- Recebe 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).
- Recebe um
CriarPedidoCommand
no corpo da requisição. - Chama
_mediator.Send(command)
, o Mediator encontra o handler certo e executa. - Recebe o resultado (
id
) e devolve noOk(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)