CQRS, or Command Query Responsibility Segregation, is a pattern that separates the models used for reading data from the models used for writing data.

Let’s see how this plays out in a typical scenario. Imagine an e-commerce system. When a customer adds an item to their cart, that’s a command. The system needs to validate the request, update the cart’s state, and persist this change. This might involve updating a database, sending a message to a queue for inventory updates, and invalidating a cached version of the cart.

Now, when that same customer views their cart to see the items they’ve added, that’s a query. The system needs to efficiently retrieve the cart’s current state. This might involve reading from a denormalized read model, which is specifically optimized for this type of query, or directly from a cache.

Here’s a simplified representation of a command handler and a query handler in pseudo-code:

// Command Handler for AddToCart
public class AddToCartCommandHandler : ICommandHandler<AddToCartCommand>
{
    private readonly IEventPublisher _eventPublisher;
    private readonly ICartRepository _cartRepository;

    public AddToCartCommandHandler(IEventPublisher eventPublisher, ICartRepository cartRepository)
    {
        _eventPublisher = eventPublisher;
        _cartRepository = cartRepository;
    }

    public async Task Handle(AddToCartCommand command)
    {
        var cart = await _cartRepository.GetCartByIdAsync(command.CartId);
        cart.AddItem(command.ProductId, command.Quantity);
        await _cartRepository.SaveAsync(cart);

        // Publish an event for other services (e.g., inventory, analytics)
        await _eventPublisher.PublishAsync(new ItemAddedToCartEvent { CartId = cart.Id, ProductId = command.ProductId, Quantity = command.Quantity });
    }
}

// Query Handler for GetCart
public class GetCartQueryHandler : IQueryHandler<GetCartQuery, CartViewModel>
{
    private readonly ICartReadModelRepository _readModelRepository;

    public GetCartQueryHandler(ICartReadModelRepository readModelRepository)
    {
        _readModelRepository = readModelRepository;
    }

    public async Task<CartViewModel> Handle(GetCartQuery query)
    {
        // Fetch from a denormalized read model optimized for display
        return await _readModelRepository.GetCartViewModelByIdAsync(query.CartId);
    }
}

The core problem CQRS solves is the impedance mismatch between the often complex, state-changing operations of writes (commands) and the simple, performance-critical nature of reads (queries). By separating these, you can optimize each path independently. The write side can focus on consistency and business logic, using rich domain models. The read side can focus on performance, using denormalized views tailored for specific query needs.

Internally, the flow typically involves:

  1. Command Handling: A command arrives, is validated, and processed by a command handler. This handler often interacts with the domain model to change state.
  2. Event Publishing: After a command successfully modifies state, relevant domain events are published. These events describe what happened.
  3. Event Handling (Read Model Updates): Other services or components subscribe to these events. Dedicated event handlers consume these events and update the read models. This is where the write side’s changes are propagated to the read side.
  4. Querying: When a query is made, it hits a query handler that retrieves data directly from the optimized read model. This read model is not the same as the domain model used for writes; it’s a materialized view.

The levers you control in CQRS are primarily around:

  • Command Design: Defining clear, concise commands that represent user intentions.
  • Domain Model: Designing rich domain models that encapsulate business rules for writes.
  • Event Design: Defining meaningful domain events that signal state changes.
  • Read Model Design: Creating specialized, denormalized views (often called projections) optimized for specific query patterns.
  • Data Stores: Choosing appropriate databases for write (e.g., relational, document) and read (e.g., document, key-value, search index) operations, which can be entirely different.
  • Message Brokers: Using queues or topics (like RabbitMQ, Kafka) to reliably deliver events from the write side to the read side.

The key to effective CQRS integration testing is to verify the entire pipeline: that a command successfully triggers state changes, that these changes are correctly published as events, and that these events are accurately processed to update the relevant read models, making the data available for subsequent queries. You’re not just testing isolated handlers; you’re testing the flow of data and state across these distinct responsibilities.

The separation of concerns means that your write models can be complex and object-oriented, focusing on rich domain logic and invariants, while your read models can be simple data structures optimized for fast retrieval. This allows for highly performant read operations without the overhead of complex joins or object-relational mapping on the read side, and for robust, consistent writes without being constrained by read performance needs.

When dealing with eventual consistency, a common pattern is to have your read models lag slightly behind your write models because of the asynchronous nature of event propagation. This means that immediately after a command is executed, a query might not reflect the latest change. This is by design and a fundamental aspect of many CQRS implementations, especially those using message queues.

Want structured learning?

Take the full Cqrs course →