Pular para conteúdo

CQRS - Command Query Responsibility Segregation

Visão Geral

CQRS (Command Query Responsibility Segregation) é um padrão arquitetural que separa as operações de leitura (Queries) das operações de escrita (Commands) em modelos distintos.

Princípio Fundamental: Um método deve ser um Command (que modifica o estado) ou uma Query (que retorna dados), mas nunca ambos.


Conceitos Principais

Commands (Comandos)

Definição: Operações que modificam o estado do sistema.

Características:

  • Representam intenções de ação (CreateUser, UpdateOrder, DeleteProduct)
  • Não retornam dados (void ou apenas ID do recurso criado)
  • Podem falhar (validações, regras de negócio)
  • São processados de forma assíncrona (opcional)
  • Disparam Domain Events

Exemplo:

public class CreateUserCommand : IRequest<Guid>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

Queries (Consultas)

Definição: Operações que retornam dados sem modificar o estado.

Características:

  • Apenas leem dados
  • Sempre retornam informações
  • Nunca modificam o estado
  • Podem ser otimizadas de forma independente
  • Podem usar modelos de leitura diferentes (DTOs)

Exemplo:

public class GetUserByIdQuery : IRequest<UserDto>
{
    public Guid Id { get; set; }
}

Arquitetura CQRS

Modelo Tradicional (Sem CQRS)

Modelo Tradicional sem CQRS

Modelo CQRS

Modelo CQRS


Benefícios do CQRS

1. Separação de Responsabilidades

  • Modelos de Escrita: Focados em validação e regras de negócio
  • Modelos de Leitura: Otimizados para performance e simplicidade

2. Escalabilidade Independente

  • Escalar leituras e escritas de forma independente
  • Read replicas para queries
  • Write masters para commands

3. Otimização de Performance

  • Queries otimizadas sem preocupação com lógica de negócio
  • Índices específicos para leitura
  • Cache agressivo no lado de leitura

4. Modelos Simplificados

  • Sem necessidade de um modelo "universal"
  • DTOs específicos para cada caso de uso
  • Redução de complexidade

5. Auditoria e Event Sourcing

  • Facilita implementação de Event Sourcing
  • Histórico completo de mudanças
  • Replay de eventos

Implementação no Santa Mão

Estrutura de Pastas

SantaMao.Core/
├── Application/
│   ├── Commands/
│   │   ├── Users/
│   │   │   ├── CreateUser/
│   │   │   │   ├── CreateUserCommand.cs
│   │   │   │   ├── CreateUserCommandHandler.cs
│   │   │   │   └── CreateUserCommandValidator.cs
│   │   │   └── UpdateUser/
│   │   │       ├── UpdateUserCommand.cs
│   │   │       └── UpdateUserCommandHandler.cs
│   │   └── ...
│   └── Queries/
│       ├── Users/
│       │   ├── GetUserById/
│       │   │   ├── GetUserByIdQuery.cs
│       │   │   ├── GetUserByIdQueryHandler.cs
│       │   │   └── GetUserByIdResponse.cs
│       │   └── SearchUsers/
│       │       ├── SearchUsersQuery.cs
│       │       └── SearchUsersQueryHandler.cs
│       └── ...

Command Handler

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
    private readonly IUserRepository _repository;
    private readonly IEventPublisher _eventPublisher;

    public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        // 1. Validação (já feita pelo FluentValidation)

        // 2. Criar agregado
        var user = User.Create(
            request.FirstName,
            request.LastName,
            request.Email,
            request.Phone
        );

        // 3. Persistir
        await _repository.AddAsync(user, cancellationToken);

        // 4. Publicar eventos
        await _eventPublisher.PublishAsync(user.DomainEvents, cancellationToken);

        // 5. Retornar ID
        return user.Id;
    }
}

Query Handler

public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
    private readonly IReadRepository _repository;
    private readonly IMapper _mapper;

    public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
    {
        // 1. Buscar dados otimizados
        var user = await _repository.GetByIdAsync(request.Id, cancellationToken);

        // 2. Mapear para DTO
        return _mapper.Map<UserDto>(user);
    }
}

Controller

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

    // Command
    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserCommand command)
    {
        var userId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetUser), new { id = userId }, userId);
    }

    // Query
    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(Guid id)
    {
        var query = new GetUserByIdQuery { Id = id };
        var user = await _mediator.Send(query);
        return Ok(user);
    }
}

Padrões e Práticas

1. Nomenclatura

Commands: Verbo no imperativo + substantivo

  • CreateUserCommand
  • UpdateOrderCommand
  • DeleteProductCommand

Queries: Get/Search + substantivo

  • GetUserByIdQuery
  • SearchUsersQuery
  • GetOrderDetailsQuery

2. Validação

Commands: Validação rigorosa com FluentValidation

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Email).EmailAddress();
    }
}

Queries: Validação mínima (apenas parâmetros básicos)

3. Retorno

Commands:

  • void (para updates/deletes)
  • Guid (ID do recurso criado)
  • Nunca retornar o objeto completo

Queries:

  • DTOs otimizados para cada caso de uso
  • Paginação para listas
  • Projeções específicas

4. MediatR

Utilizamos MediatR como mediador entre Controllers e Handlers:

services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

Quando Usar e Quando NÃO Usar

✅ Usar CQRS Quando:

  • Leituras e escritas têm características muito diferentes
  • Performance de leitura é crítica
  • Modelos de escrita complexos com muitas regras de negócio
  • Escalabilidade independente é necessária
  • Sistema complexo que se beneficia da separação

❌ NÃO Usar CQRS Quando:

  • CRUD simples sem regras de negócio complexas
  • Sistema pequeno com poucos usuários
  • Performance não é crítica
  • Equipe pequena sem experiência no padrão
  • Deadline curto e separação adiciona complexidade desnecessária

Relacionamento com Outros Padrões

CQRS trabalha em conjunto com:

  • DDD - Commands executam métodos do Domain Model, respeitam invariantes e disparam Domain Events
  • EDA - Commands publicam eventos após operações bem-sucedidas
  • Outbox Pattern - Garante que eventos de Commands sejam publicados com segurança

Fluxo Integrado:

Command → Handler → Aggregate (DDD) → Domain Event → Outbox → Redpanda (EDA)

Conclusão

CQRS no Santa Mão traz clareza e performance para aplicações complexas através da separação entre leituras e escritas.

Benefícios Principais:

✅ Separação clara entre leituras e escritas
✅ Performance otimizada para cada caso de uso
✅ Código mais limpo e manutenível
✅ Escalabilidade independente

A implementação consistente deste padrão em todo o projeto garante que desenvolvedores possam navegar facilmente pelo código e entender rapidamente o fluxo de dados e lógica de negócio.


Última atualização: Novembro 2025
Maintainers: Equipe de Arquitetura Santa Mão