CQRS na prática: separando leituras e escritas complexas
1. Fundamentos do CQRS e quando aplicá-lo
1.1. Definição e origem do padrão Command Query Responsibility Segregation
CQRS (Command Query Responsibility Segregation) foi popularizado por Greg Young e Udi Dahan como uma evolução do princípio CQS (Command Query Separation) proposto por Bertrand Meyer. Enquanto CQS opera no nível de métodos dentro de uma classe, CQRS eleva a separação ao nível arquitetural, criando modelos distintos para comandos (escritas) e consultas (leituras).
1.2. Diferença fundamental entre comandos e consultas
Comandos são operações que alteram o estado do sistema e não devem retornar dados de domínio. Consultas retornam dados e não devem alterar estado. Essa separação permite otimizar cada lado independentemente.
// Comando: não retorna dados de domínio
public record ProcessarPedidoCommand(
Guid PedidoId,
Guid ClienteId,
List<ItemPedido> Itens,
EnderecoEntrega Endereco
);
// Consulta: não altera estado
public record ObterDashboardQuery(
Guid ClienteId,
DateTime DataInicio,
DateTime DataFim
);
1.3. Cenários ideais
CQRS brilha em sistemas com:
- Assimetria de carga entre leituras e escritas (ex: 90% consultas, 10% comandos)
- Regras de negócio complexas que exigem validações multi-domínio
- Múltiplas visões dos mesmos dados para diferentes contextos
2. Arquitetura de separação de modelos
2.1. Modelo de escrita
O modelo de escrita utiliza agregados DDD, garantindo consistência imediata através de validações invariantes.
public class Pedido : AggregateRoot
{
private List<ItemPedido> _itens = new();
public Guid Id { get; private set; }
public StatusPedido Status { get; private set; }
public decimal ValorTotal => _itens.Sum(i => i.Quantidade * i.PrecoUnitario);
public void AdicionarItem(Produto produto, int quantidade)
{
if (quantidade <= 0)
throw new DomainException("Quantidade deve ser positiva");
if (produto.EstoqueDisponivel < quantidade)
throw new DomainException("Estoque insuficiente");
_itens.Add(new ItemPedido(produto.Id, quantidade, produto.Preco));
AddDomainEvent(new ItemAdicionadoAoPedidoEvent(Id, produto.Id, quantidade));
}
}
2.2. Modelo de leitura
O modelo de leitura é otimizado para consultas, utilizando denormalização e visões materializadas.
public class PedidoReadModel
{
public Guid Id { get; set; }
public string ClienteNome { get; set; }
public string Status { get; set; }
public decimal ValorTotal { get; set; }
public int QuantidadeItens { get; set; }
public DateTime DataCriacao { get; set; }
public DateTime? DataPagamento { get; set; }
}
2.3. Handlers separados
// Command Handler
public class ProcessarPedidoHandler : ICommandHandler<ProcessarPedidoCommand>
{
private readonly IPedidoRepository _repository;
private readonly IEventBus _eventBus;
public async Task Handle(ProcessarPedidoCommand command, CancellationToken ct)
{
var pedido = new Pedido(command.ClienteId);
foreach (var item in command.Itens)
pedido.AdicionarItem(item.Produto, item.Quantidade);
await _repository.SaveAsync(pedido, ct);
await _eventBus.PublishAsync(new PedidoProcessadoEvent(pedido.Id), ct);
}
}
// Query Handler
public class ObterDashboardHandler : IQueryHandler<ObterDashboardQuery, DashboardDto>
{
private readonly IDashboardReadRepository _repository;
public async Task<DashboardDto> Handle(ObterDashboardQuery query, CancellationToken ct)
{
return await _repository.ObterDashboardAsync(
query.ClienteId, query.DataInicio, query.DataFim, ct);
}
}
3. Sincronização entre modelos: eventos e projeções
3.1. Event Sourcing como fonte única de verdade
Event Sourcing armazena o estado como uma sequência de eventos, permitindo reconstruir qualquer estado anterior e alimentar projeções de leitura.
public class PedidoEventStore
{
private readonly List<IEvent> _events = new();
public void Append(IEvent @event)
{
_events.Add(@event);
// Persistir em banco de eventos
}
public IEnumerable<IEvent> GetEvents(Guid aggregateId)
{
return _events.Where(e => e.AggregateId == aggregateId);
}
}
3.2. Projeções assíncronas
public class PedidoProjection : IEventHandler<PedidoProcessadoEvent>
{
private readonly IPedidoReadRepository _readRepo;
public async Task Handle(PedidoProcessadoEvent @event, CancellationToken ct)
{
var readModel = new PedidoReadModel
{
Id = @event.PedidoId,
Status = "Processado",
DataCriacao = DateTime.UtcNow,
ValorTotal = @event.ValorTotal,
QuantidadeItens = @event.Itens.Count
};
await _readRepo.UpsertAsync(readModel, ct);
}
}
3.3. Consistência eventual
Estratégias para lidar com dados obsoletos:
- Timestamp de última atualização no modelo de leitura
- Versionamento otimista para detecção de conflitos
- Notificações ao usuário sobre dados "quase em tempo real"
4. Implementação prática de comandos complexos
4.1. Processamento de pedido multi-domínio
public class ProcessarPedidoCompletoHandler : ICommandHandler<ProcessarPedidoCompletoCommand>
{
private readonly IPedidoRepository _pedidoRepo;
private readonly IEstoqueRepository _estoqueRepo;
private readonly IFaturamentoService _faturamentoService;
private readonly IUnitOfWork _uow;
public async Task Handle(ProcessarPedidoCompletoCommand command, CancellationToken ct)
{
using var transaction = await _uow.BeginTransactionAsync(ct);
try
{
var pedido = new Pedido(command.ClienteId);
// Validações multi-domínio
foreach (var item in command.Itens)
{
var produto = await _estoqueRepo.ObterPorIdAsync(item.ProdutoId, ct);
if (produto == null || produto.EstoqueDisponivel < item.Quantidade)
throw new DomainException($"Estoque insuficiente para {item.ProdutoId}");
pedido.AdicionarItem(produto, item.Quantidade);
produto.DiminuirEstoque(item.Quantidade);
await _estoqueRepo.AtualizarAsync(produto, ct);
}
await _pedidoRepo.SaveAsync(pedido, ct);
await _faturamentoService.CriarFaturaAsync(pedido, ct);
await _uow.CommitAsync(ct);
}
catch
{
await _uow.RollbackAsync(ct);
throw;
}
}
}
4.2. Idempotência em comandos
public class IdempotentCommandHandler<T> : ICommandHandler<T> where T : ICommand
{
private readonly ICommandHandler<T> _inner;
private readonly IIdempotencyStore _store;
public async Task Handle(T command, CancellationToken ct)
{
var key = command.GetIdempotencyKey();
if (await _store.ExistsAsync(key, ct))
return; // Comando já processado
await _inner.Handle(command, ct);
await _store.MarkAsProcessedAsync(key, ct);
}
}
5. Implementação prática de consultas complexas
5.1. Dashboard agregado multi-bounded context
public class DashboardReadRepository : IDashboardReadRepository
{
private readonly DbContext _db;
public async Task<DashboardDto> ObterDashboardAsync(
Guid clienteId, DateTime inicio, DateTime fim, CancellationToken ct)
{
var query = from p in _db.PedidosReadModel
join f in _db.FaturamentoReadModel on p.Id equals f.PedidoId
join e in _db.EntregasReadModel on p.Id equals e.PedidoId
where p.ClienteId == clienteId
&& p.DataCriacao >= inicio && p.DataCriacao <= fim
select new DashboardDto
{
TotalPedidos = p.Count,
ValorTotalFaturado = f.ValorTotal,
StatusEntrega = e.Status,
MediaItensPorPedido = p.QuantidadeItens / p.Count
};
return await query.FirstOrDefaultAsync(ct);
}
}
5.2. Visões materializadas
-- SQL para criar visão materializada
CREATE MATERIALIZED VIEW dashboard_mensal AS
SELECT
cliente_id,
DATE_TRUNC('month', data_criacao) AS mes,
COUNT(*) AS total_pedidos,
SUM(valor_total) AS receita_total,
AVG(quantidade_itens) AS media_itens
FROM pedidos_read_model
GROUP BY cliente_id, DATE_TRUNC('month', data_criacao);
6. Integração com microsserviços
6.1. Comunicação via mensageria
// Publicação de comando via Kafka
public class KafkaCommandBus : ICommandBus
{
private readonly IProducer<string, string> _producer;
public async Task SendAsync<T>(T command, CancellationToken ct) where T : ICommand
{
var message = new Message<string, string>
{
Key = command.GetType().Name,
Value = JsonSerializer.Serialize(command)
};
await _producer.ProduceAsync("commands-topic", message, ct);
}
}
6.2. Observabilidade com service mesh
O service mesh (Istio/Linkerd) adiciona:
- Roteamento inteligente entre command handlers e query handlers
- Circuit breakers para comandos com alta latência
- Métricas de sucesso/falha por tipo de operação
6.3. Sagas para orquestração
public class PedidoSaga : Saga<PedidoSagaData>
{
public async Task ProcessarPedido(ProcessarPedidoCommand command)
{
await SendCommand(new ReservarEstoqueCommand(command.Itens));
await SendCommand(new ProcessarPagamentoCommand(command.ClienteId, command.Valor));
await SendCommand(new CriarNotaFiscalCommand(command.PedidoId));
}
public async Task Compensar(ProcessarPedidoCommand command)
{
await SendCommand(new CancelarReservaEstoqueCommand(command.Itens));
await SendCommand(new EstornarPagamentoCommand(command.ClienteId, command.Valor));
}
}
7. Desafios, armadilhas e boas práticas
7.1. Complexidade adicional
- Manter dois modelos aumenta o esforço de desenvolvimento
- Sincronização eventual pode causar inconsistências temporárias
- Debugging mais complexo com fluxos assíncronos
7.2. Quando evitar CQRS
- Sistemas CRUD simples sem regras de negócio complexas
- Requisitos de latência extremamente baixa (< 10ms)
- Equipes pequenas sem experiência em arquiteturas distribuídas
7.3. Padrões complementares
- DDD + Bounded Contexts: delimita claramente os limites de cada modelo
- Event Sourcing: fornece a fonte única de verdade para projeções
- Eventual Consistency: aceita que leituras podem estar ligeiramente desatualizadas
Referências
- Microsoft - CQRS Pattern — Documentação oficial da Microsoft sobre o padrão CQRS com exemplos práticos e considerações de design
- Martin Fowler - CQRS — Artigo seminal de Martin Fowler explicando os conceitos fundamentais e quando aplicar CQRS
- Greg Young - CQRS Documents — Documento original de Greg Young detalhando a teoria e prática do CQRS
- Udi Dahan - Clarified CQRS — Artigo clássico de Udi Dahan esclarecendo mitos e armadilhas comuns do CQRS
- Event Store - CQRS and Event Sourcing — Guia prático do Event Store sobre implementação de CQRS com Event Sourcing
- RabbitMQ - CQRS with Messaging — Tutorial oficial do RabbitMQ demonstrando comunicação assíncrona entre comandos e consultas
- Istio - Microservices Resilience — Documentação do Istio mostrando como service mesh pode melhorar resiliência em arquiteturas CQRS