6 min read

Q52 — What is CQRS — why would you separate reads from writes at the architecture level?


What is the conventional model?

In a typical system, every operation goes to the same database through the same model. You write an order: INSERT INTO orders .... You read an order: SELECT * FROM orders WHERE id = ?. You search for orders: SELECT * FROM orders WHERE user_id = ? AND status = 'PENDING' ORDER BY created_at LIMIT 20. All of these — write, single read, search — hit the same table, the same schema, the same database.

This is simple. For most systems, it's the right default. But as systems grow, the requirements of reads and writes start to diverge in ways that a single model struggles to satisfy simultaneously.


Why do read and write requirements diverge?

Consider what writes need: consistency, integrity, transactional safety. A write must enforce business rules — you can't sell an item that's not in inventory, you can't charge a negative amount, you can't create two orders for the same idempotency key. Writes are naturally normalized — you store data once, in the right place, with foreign keys and constraints. Writes benefit from ACID guarantees.

Now consider what reads need: performance, flexibility, specific shapes of data. A product listing page needs the product name, price, thumbnail, average rating, number of reviews, and whether it's in stock — data that comes from four different tables. Every request joins those four tables. A dashboard needs aggregated data across millions of rows. A search needs full-text capabilities. A timeline needs data sorted, paginated, and filtered in complex ways.

The write model (normalized, transactional, consistent) is optimized for correctness. The read model (denormalized, precomputed, flexible) is optimized for query performance. These two goals are in tension. A highly normalized schema that's great for writes requires expensive joins for reads. A denormalized schema that's great for reads is redundant and hard to keep consistent on writes.

CQRS (Command Query Responsibility Segregation) resolves this tension by giving each concern its own model.


What does CQRS actually mean?

CQRS separates every operation into one of two categories:

Commands: Intent to change state. "Place this order." "Update this user's address." "Cancel this subscription." Commands go through the write model — the normalized, transactional, integrity-enforcing side. Commands return only success/failure, not data.

Queries: Requests for data. "Show me this user's order history." "What are the top-selling products this week?" "What is the current inventory level?" Queries go through the read model — the denormalized, precomputed, query-optimized side. Queries never modify state.

The separation can be shallow (same database, separate code paths) or deep (separate databases entirely, read model maintained asynchronously from the write model).

In the shallow form, CQRS is mostly a code architecture pattern: the application has a command handler layer and a query handler layer, each using different code but the same underlying database. The benefit is conceptual clarity — operations that change state are explicit commands, operations that read state are explicit queries.

In the deep form — which is what most people mean when they say "CQRS" in system design — the read model is a separate data store (or multiple stores), each optimized for the specific queries it must serve. The write model publishes events when state changes. The read model consumes those events and maintains its own precomputed view.


What does the deep form look like in practice?

Suppose you're building a product catalog. The write side: a normalized relational database with products, categories, pricing, and inventory tables. A command to update the product price writes to the pricing table in a transaction. Integrity is enforced. This is the authoritative source of truth.

The read side: an Elasticsearch index for full-text search and faceted filtering. A Redis cache with precomputed product cards for fast listing pages. A read replica for analytics queries.

When the product's price changes (write side), the write model publishes a "ProductPriceUpdated" event. A consumer picks this up and updates Elasticsearch, the Redis cache, and any other read models that care about prices.

Now:

  • Search queries go to Elasticsearch (optimized for full-text, facets, ranking)
  • Listing page queries go to Redis (precomputed, sub-millisecond)
  • Analytics queries go to the read replica (large scans without blocking writes)
  • Price updates go to the relational DB (transactional, normalized)

Each store does what it's best at. The data is the same — a product — but the representations are different, each tailored to the access pattern.


What does CQRS cost?

Eventual consistency. The read models are updated asynchronously after the write succeeds. There is a window — usually milliseconds, but potentially longer if the event consumer is lagging — where the read model is stale. A user updates their email address; immediately refreshing their profile might show the old email. This is the same read-your-own-writes problem from replication (Q35). Solutions: optimistic UI updates, synchronous projection for critical reads, versioned reads with "wait until version N is visible."

Complexity of maintaining multiple models. The read models must be kept in sync with the write model. This requires event publishing (ideally via the outbox pattern, Q49), event consumers, projection logic, and monitoring for consumer lag. If a read model gets corrupted or falls behind, it must be rebuilt by replaying events from the write model — which requires the write model to be an event store or to have change data capture enabled.

Increased operational surface. More databases to operate, monitor, and back up. More consumers to keep healthy. More things that can go wrong.

Complexity of consistency reasoning. Developers must understand which read model is appropriate for which query, and must account for the staleness window. Ad-hoc queries that could previously be written as simple SQL against one database now require knowing the right store.


When is CQRS worth it?

CQRS adds complexity. It's worth it when:

The read access pattern is fundamentally different from the write model's structure — multiple tables must be joined, complex aggregations must be precomputed, different query engines are needed (full-text search, time-series, graph).

The read load is orders of magnitude higher than the write load — 99% of traffic is reads, scaling the write database for read volume is wasteful and slow.

You're using event sourcing (Q51) — CQRS is almost mandatory when using event sourcing, because replaying events to build current state is expensive and projections (read models) are the natural solution.

Different teams own reads vs writes, and you want them to evolve independently.

CQRS is the wrong choice for simple CRUD systems, small teams, or early-stage products where the operational complexity is unjustified.


The mental model

code
Conventional model:
  Same DB, same schema, same model for reads and writes
  Write needs: normalization, consistency, integrity
  Read needs: denormalization, aggregation, flexibility, speed
  → These conflict → can't fully optimize for both

CQRS = Command Query Responsibility Segregation
  Commands (writes): change state → write model (normalized, transactional)
  Queries (reads): return data → read model (denormalized, query-optimized)
  Commands return success/failure only
  Queries never change state

Shallow CQRS: same DB, separate code paths (clarity only, no perf win)
Deep CQRS: separate data stores per read pattern
  Write model → publishes events → read model consumers update projections

Example:
  Write model: relational DB (orders, payments, normalized, ACID)
  Read models:
    Elasticsearch: full-text search, faceted filtering
    Redis: precomputed listing cards (sub-ms reads)
    Read replica: analytics (large scans, no blocking writes)

Each store optimized for its access pattern

Costs:
  Eventual consistency: read model lags behind write model
    → stale reads, read-your-own-writes problem
  Operational complexity: multiple stores, consumers, lag monitoring
  Projection maintenance: corruption → replay from write model (needs events or CDC)

Worth it when:
  Read and write access patterns fundamentally differ
  Read load >> write load (scale them independently)
  Using event sourcing (projections are mandatory anyway)
  Teams own reads and writes independently

Not worth it for:
  Simple CRUD (one model works fine)
  Small teams, early products
  Minimal performance pressure on reads

CQRS + Event Sourcing:
  Event store = write model (append-only log of events)
  Projections = read models (built from replaying events)
  Natural fit — event store IS the source of truth, projections are views

CQRS exists because the data model that's best for writing is rarely the model that's best for reading. Separating these concerns lets each side be independently optimized, scaled, and evolved — at the cost of eventual consistency and operational overhead. The deeper the separation, the more power and the more complexity.