CQRS is often presented as a way to decouple read and write operations, but its real magic is how it fundamentally changes your understanding of data state.

Imagine you have a simple e-commerce system. A user adds an item to their cart.

// Write Operation: Command
{
  "commandType": "AddItemToCart",
  "cartId": "c123",
  "itemId": "item456",
  "quantity": 1
}

This command hits your "Order Service." It validates the request, perhaps checks inventory, and then publishes an event:

// Write Operation: Event
{
  "eventType": "ItemAddedToCart",
  "cartId": "c123",
  "itemId": "item456",
  "quantity": 1,
  "timestamp": "2023-10-27T10:00:00Z"
}

Now, a separate "Cart Read Model Service" subscribes to ItemAddedToCart events. It takes this event and updates its own denormalized, query-optimized data store. This read model might look like:

// Read Model (denormalized for querying)
{
  "cartId": "c123",
  "items": [
    {
      "itemId": "item456",
      "quantity": 1
    }
  ],
  "lastUpdated": "2023-10-27T10:00:00Z"
}

When a user requests to view their cart, they hit the "Cart Read Model Service," which directly queries its optimized store. No complex joins, no waiting for write operations to complete. The data is already in the perfect shape for reading.

CQRS, at its core, means that your write-side concerns (commands, validation, business logic) are entirely separate from your read-side concerns (querying, data shape, performance). In a microservices architecture, this separation is amplified. Each service can have its own command handlers and its own read models, tailored to its specific needs. The "Order Service" might focus on transactional integrity, while the "Cart Read Model Service" prioritizes low-latency cart retrieval.

The key is that the write side produces a stream of events, and the read side consumes these events to build its own, independent view of the world. This leads to incredible scalability and resilience. If your read model database goes down, it doesn’t affect the ability to place orders. You can simply re-hydrate the read model from the event stream once it’s back up. You can also have multiple read models for the same write-side data, each optimized for different query patterns. For instance, you might have one read model for displaying the cart contents and another for generating abandoned cart reports.

The event ItemAddedToCart is not just a notification; it’s a definitive statement of a change in state. The read model doesn’t ask the write side "what’s in the cart?"; it is told "an item was added to the cart" and reacts accordingly. This event-driven nature means that the read model is always eventually consistent with the write model, but the consistency is achieved asynchronously, allowing for independent scaling and fault tolerance.

Most people think of CQRS as just separating read and write databases. What’s often overlooked is the implication of that separation on how you model your data. The write side doesn’t maintain a single, canonical "customer" record that is then queried. Instead, the write side emits CustomerCreated, CustomerAddressUpdated, CustomerEmailChanged events. A "Customer Read Service" might then consume all of these to build a denormalized CustomerProfile object that includes their current address and email, optimized for displaying on a profile page, while a different "Customer Analytics Service" might consume the same events to build a different data structure for analyzing customer demographics. The read models are projections of the events, not direct reflections of the write-side’s internal state.

The next challenge is managing eventual consistency across multiple read models and handling complex queries that might span data from several services.

Want structured learning?

Take the full Cqrs course →