CQRS is often pitched as a way to optimize read performance, but its true power lies in decoupling the write and read sides of your domain, allowing them to evolve independently.

Let’s imagine a simple e-commerce order system.

Write Side (Command Side):

When a customer places an order, a PlaceOrderCommand is sent. This command is handled by an OrderCommandHandler.

public class OrderCommandHandler : ICommandHandler<PlaceOrderCommand>
{
    private readonly IEventStore _eventStore;
    private readonly IOrderRepository _orderRepository; // For validation

    public OrderCommandHandler(IEventStore eventStore, IOrderRepository orderRepository)
    {
        _eventStore = eventStore;
        _orderRepository = orderRepository;
    }

    public async Task Handle(PlaceOrderCommand command)
    {
        // Domain logic and validation happens here before generating events
        var order = new Order(command.OrderId, command.CustomerId, command.Items);
        await _orderRepository.SaveAsync(order); // Example validation check

        var orderPlacedEvent = new OrderPlacedEvent(command.OrderId, command.CustomerId, command.Items, DateTime.UtcNow);
        await _eventStore.AppendAsync(orderPlacedEvent);
    }
}

Read Side (Query Side):

A separate OrderReadModel is populated by an event handler that listens for OrderPlacedEvent. This read model is optimized for querying.

public class OrderPlacedEventHandler : IEventHandler<OrderPlacedEvent>
{
    private readonly IOrderReadRepository _orderReadRepository;

    public OrderPlacedEventHandler(IOrderReadRepository orderReadRepository)
    {
        _orderReadRepository = orderReadRepository;
    }

    public async Task Handle(OrderPlacedEvent eventData)
    {
        var orderSummary = new OrderSummary
        {
            OrderId = eventData.OrderId,
            CustomerId = eventData.CustomerId,
            OrderDate = eventData.OrderDate,
            TotalItems = eventData.Items.Count
        };
        await _orderReadRepository.UpsertAsync(orderSummary);
    }
}

The system works by having commands trigger domain logic and produce events. These events are then consumed by separate handlers that update read models. This separation means your write model can be transactional and complex, while your read models can be denormalized and highly performant for specific queries.

The most surprising truth about CQRS is that the "R" (Read) side doesn’t have to be a separate database or even a different technology stack; it’s a conceptual separation. You can have a single database where different tables or views serve as your read models, as long as they are populated asynchronously from events and are optimized for querying, distinct from your write-side aggregates.

You control the complexity of your write model independently of the performance characteristics of your read models. This means you can have a highly normalized, transactional write side that enforces business rules rigorously, while your read side can be a denormalized, query-optimized structure that scales horizontally for high read throughput. The "eventual consistency" is the trade-off for this decoupling, and understanding its implications is key.

The critical levers you control are the definition of your commands and events, the domain logic within your command handlers, and the structure and population of your read models. Each read model can be tailored to a specific query pattern, leading to dramatic performance improvements for frequently accessed data.

It’s a common misconception that CQRS requires a complex event sourcing setup. While event sourcing is a natural fit and often used with CQRS, it’s not a prerequisite. You can implement CQRS with a traditional database for your write side, emitting domain events after a successful save, and then have those events update separate read models. The core principle is the separation of concerns between initiating state changes and querying state.

The next challenge you’ll face is handling queries that span multiple read models or require complex aggregations not directly supported by your denormalized views.

Want structured learning?

Take the full Cqrs course →