CQRS and Event Sourcing
Separate reads and writes with CQRS and Event Sourcing patterns.
By EMEPublished: February 20, 2025
cqrsevent sourcingarchitecturedesign patternsdomain-driven-design
A Simple Analogy
CQRS is like having separate checkout and returns lines. Commands (writes) go one way, Queries (reads) go another. Event Sourcing is like writing every transaction in a journal instead of just the current balance.
What Is CQRS?
CQRS (Command Query Responsibility Segregation) separates read and write operations into different models. Queries read from optimized views, Commands update the event store.
Why Use CQRS + Event Sourcing?
- Scalability: Scale reads and writes independently
- Auditability: Every change is an event
- Temporal queries: Ask "what was state at time X"
- Debugging: Complete history of changes
- Event-driven: Trigger workflows on events
Event Store
public interface IEvent
{
Guid AggregateId { get; }
DateTime Timestamp { get; }
}
public class OrderPlaced : IEvent
{
public Guid AggregateId { get; set; }
public DateTime Timestamp { get; set; }
public string CustomerId { get; set; }
public decimal Total { get; set; }
}
public class OrderShipped : IEvent
{
public Guid AggregateId { get; set; }
public DateTime Timestamp { get; set; }
public string TrackingNumber { get; set; }
}
Event Store Implementation
public class EventStore
{
private readonly List<IEvent> _events = new();
public void AppendEvent(IEvent @event)
{
@event.Timestamp = DateTime.UtcNow;
_events.Add(@event);
}
public List<IEvent> GetEvents(Guid aggregateId)
{
return _events.Where(e => e.AggregateId == aggregateId).ToList();
}
public Order ReplayEvents(Guid orderId)
{
var order = new Order { Id = orderId };
var events = GetEvents(orderId);
foreach (var @event in events)
{
order = @event switch
{
OrderPlaced op => order with { CustomerId = op.CustomerId, Total = op.Total },
OrderShipped os => order with { TrackingNumber = os.TrackingNumber },
_ => order
};
}
return order;
}
}
Command Handler
public class PlaceOrderCommand
{
public Guid OrderId { get; set; }
public string CustomerId { get; set; }
public decimal Total { get; set; }
}
public class PlaceOrderHandler
{
private readonly EventStore _eventStore;
public void Handle(PlaceOrderCommand command)
{
var @event = new OrderPlaced
{
AggregateId = command.OrderId,
CustomerId = command.CustomerId,
Total = command.Total
};
_eventStore.AppendEvent(@event);
}
}
Read Model (Projection)
public class OrderReadModel
{
private readonly Dictionary<Guid, OrderDto> _cache = new();
public void HandleOrderPlaced(OrderPlaced @event)
{
_cache[@event.AggregateId] = new OrderDto
{
Id = @event.AggregateId,
CustomerId = @event.CustomerId,
Status = "Placed"
};
}
public void HandleOrderShipped(OrderShipped @event)
{
_cache[@event.AggregateId].Status = "Shipped";
_cache[@event.AggregateId].TrackingNumber = @event.TrackingNumber;
}
public OrderDto GetOrder(Guid id) => _cache[id];
}
Best Practices
- Immutable events: Never modify recorded events
- Event versioning: Handle schema changes
- Snapshots: Cache state for old aggregates
- Projections: Keep read models updated
- Idempotency: Handle duplicate events
Related Concepts
- Sagas for long-running processes
- Event bus for event distribution
- Snapshot strategy for performance
- Temporal queries and analytics
Summary
CQRS separates reads from writes, while Event Sourcing records every change as an immutable event. Together, they enable auditability, temporal queries, and independent scalability.