Isaac.

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

  1. Immutable events: Never modify recorded events
  2. Event versioning: Handle schema changes
  3. Snapshots: Cache state for old aggregates
  4. Projections: Keep read models updated
  5. 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.