CQRS & Event Sourcing
Separacao de leitura e escrita, armazenamento de eventos e reconstrucao de estado
Principio: CQRS separa o modelo de escrita (commands) do modelo de leitura (queries). Event Sourcing armazena eventos em vez de estado β o estado e derivado ao reproduzir os eventos.
Command Query Responsibility Segregation
CQRS e o principio de que o modelo usado para escrever dados (commands) deve ser diferente do modelo usado para ler dados (queries). Cada lado pode ser otimizado, escalado e evoluido independentemente.
Fluxo: Commands vs Queries
Por que separar leitura e escrita?
| Aspecto | Leitura (Query) | Escrita (Command) |
|---|---|---|
| Escala | 10-100x mais frequente. Replicas read-only. | Menos frequente, exige consistencia forte. |
| Otimizacao | Views denormalizadas, caching agressivo. | Modelo normalizado, regras de negocio complexas. |
| Modelo | DTOs flat, materialized views. | Aggregates ricos, domain events, invariantes. |
| Seguranca | Permissoes mais liberais. | Permissoes restritas, auditoria detalhada. |
Tradicional CRUD vs CQRS
| Aspecto | CRUD Tradicional | CQRS |
|---|---|---|
| Modelo | Unico β mesmo modelo para ler e escrever | Separado β write model + read model |
| Escalabilidade | Escala junto (read e write competem pelo mesmo recurso) | Escala independente (read replica, cache, denormalizacao) |
| Complexidade | Baixa β simples e direto | Alta β mais infraestrutura e codigo |
| Consistencia | Forte β leitura imediata | Eventual β read model pode estar atrasado |
Commands β alteracoes de estado
Commands representam intencoes de mudar o estado do sistema. Sao imperativos: "crie pedido", "adicione item", "cancele pedido". Nao retornam dados β retornam void ou no maximo um Id.
// Commands β intencoes de alterar estado
public record CreateOrderCommand(Guid CustomerId, List<OrderItemDto> Items);
public record UpdateOrderCommand(Guid OrderId, List<OrderItemDto> Items);
public record CancelOrderCommand(Guid OrderId, string Reason);
// Handler retorna void ou Id β nunca retorna dados de leitura
public class CreateOrderHandler
{
public async Task<Guid> HandleAsync(CreateOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId);
foreach (var item in cmd.Items)
order.AddItem(item.ProductName, item.Qty, item.Price);
await _repo.SaveAsync(order, ct);
return order.Id;
}
}
Queries β leitura de dados
Queries nao alteram estado. Retornam DTOs otimizados para a tela que vai consumir. Podem usar views denormalizadas, caches, ou bancos de leitura separados.
// Queries β leitura de dados, sem side effects
public record GetOrderQuery(Guid OrderId);
public record GetOrdersListQuery(Guid? CustomerId, int Page, int PageSize);
// DTOs otimizados para a tela
public record OrderDto(Guid Id, string CustomerName, decimal Total, string Status, DateTime CreatedAt);
// Handler le do read model (view, cache, ou DB separado)
public class GetOrderQueryHandler
{
public async Task<OrderDto?> HandleAsync(GetOrderQuery query, CancellationToken ct)
=> await _readDb.OrderSummaries
.Where(o => o.OrderId == query.OrderId)
.Select(o => new OrderDto(o.OrderId, o.CustomerName, o.Total, o.Status, o.CreatedAt))
.FirstOrDefaultAsync(ct);
}
Importante: CQRS nao exige Event Sourcing. Voce pode usar CQRS com um unico banco de dados β basta separar os modelos de leitura e escrita no codigo. Event Sourcing e uma estrategia de persistencia ortogonal.
Quando CQRS vale a complexidade extra?
CQRS vale quando: 1) Read e write tem requisitos de escala muito diferentes. 2) O modelo de dominio e complexo e voce quer proteger invariantes no write side. 3) Voce precisa de projecoes otimizadas para diferentes consumidores (API, relatorio, busca). Nao vale para CRUDs simples β a complexidade adicional nao se justifica.