The read side of a Command Query Responsibility Segregation (CQRS) system can scale independently from the write side because it’s a fundamentally different beast, optimized for querying rather than for state changes.

Let’s see this in action. Imagine we have an e-commerce system. The write side handles placing orders, updating inventory, and processing payments. The read side, however, is all about showing customers product catalogs, order history, and current stock levels.

Here’s a simplified view of how the read side might be structured.

Write Side (Order Service):

{
  "commandType": "PlaceOrder",
  "payload": {
    "orderId": "ORD12345",
    "customerId": "CUST987",
    "items": [
      {"productId": "PROD001", "quantity": 2},
      {"productId": "PROD002", "quantity": 1}
    ],
    "shippingAddress": "123 Main St"
  }
}

When this command is processed, the write side updates the authoritative state (e.g., in a relational database).

Event Emission (Write Side):

After successfully processing the command, the write side publishes an event:

{
  "eventType": "OrderPlaced",
  "payload": {
    "orderId": "ORD12345",
    "customerId": "CUST987",
    "items": [
      {"productId": "PROD001", "quantity": 2},
      {"productId": "PROD002", "quantity": 1}
    ],
    "orderTimestamp": "2023-10-27T10:00:00Z"
  }
}

Read Side (Order Read Model Service):

This service subscribes to OrderPlaced events. It doesn’t execute commands; it reacts to events and updates its own data store, which is optimized for queries.

Read Side Data Store (e.g., a NoSQL document database):

{
  "orderId": "ORD12345",
  "customerId": "CUST987",
  "items": [
    {"productId": "PROD001", "quantity": 2, "productName": "Widget A"},
    {"productId": "PROD002", "quantity": 1, "productName": "Gadget B"}
  ],
  "orderTimestamp": "2023-10-27T10:00:00Z",
  "status": "Processing"
}

Notice how the read model might denormalize data (like productName) that wasn’t directly in the OrderPlaced event, but was fetched from another service (e.g., a Product Catalog Service) during event processing. This makes queries faster.

The Problem This Solves:

The core problem is that the optimal way to handle writes (which often requires ACID transactions, strong consistency, and potentially complex business logic) is usually very different from the optimal way to handle reads (which prioritize speed, availability, and the ability to serve many concurrent requests with diverse query patterns). Trying to make a single data store do both efficiently is a recipe for disaster at scale.

How It Works Internally:

  1. Write Side (Command Handling): Receives commands, validates them, applies business logic, and updates the primary write-side data store. Crucially, it then emits events that capture the state changes that occurred.
  2. Event Bus/Message Queue: A central mechanism (like Kafka, RabbitMQ, or Azure Service Bus) where events are published and subscribed to. This decouples the write side from the read side. The write side doesn’t need to know who is listening to its events.
  3. Read Side (Event Handling/Projection): One or more services that subscribe to relevant events from the bus. Each read side service (or "projection") is responsible for updating its own dedicated read-model data store. This data store is typically optimized for the specific query patterns it needs to support (e.g., a document database for flexible querying, a columnar store for analytics, or a search index for full-text search).
  4. Querying: Clients query the read-side data stores directly. Since these stores are optimized for reads, they can handle high volumes of queries very efficiently.

The Levers You Control:

  • Write-Side Data Store: Choose a store that excels at transactional consistency and handling complex state mutations.
  • Eventual Consistency: Understand and embrace that read models will be eventually consistent with the write side. The lag is usually measured in milliseconds or seconds, which is acceptable for most read operations.
  • Read-Side Data Store(s): This is where the magic of independent scaling happens. You can have multiple read models, each using a different database technology perfectly suited for its specific queries. For example:
    • A product catalog read model in Elasticsearch for fast, full-text search.
    • An order history read model in MongoDB for flexible querying by customer or date.
    • An inventory snapshot read model in Redis for extremely low-latency stock checks.
  • Eventual Consistency Lag: Monitor the delay between a write operation completing and the read models reflecting that change. Tune your event processing and read-side infrastructure to meet your application’s latency requirements.
  • Replayability: The event log is a replayable history. If a read model gets corrupted or needs to be rebuilt, you can simply replay events from the event bus to recreate its state without affecting the write side.

The independence of scaling comes from the fact that you can throw more resources (CPU, memory, read replicas) at your read-side databases without impacting the write side’s performance or vice-versa. If your product catalog queries explode, you scale your Elasticsearch cluster. If order placement becomes a bottleneck, you scale your write-side database cluster. They are entirely separate concerns and separate scaling dimensions.

When designing your read models, it’s crucial to consider how data is aggregated and transformed. For instance, a single OrderPlaced event might trigger updates across several different read models, each tailored for a specific reporting or display purpose, further illustrating the flexibility and independent nature of the read side.

Want structured learning?

Take the full Cqrs course →