Skip to main content

CQRS: When to Separate Reads and Writes

What This Concept Is

CQRS -- Command Query Responsibility Segregation -- is the design choice where the model you use to change state (the write model, driven by commands) is separate from the model(s) you use to read state (read models, optimized for queries).

In a traditional architecture:

Client --(read or write)--> one domain model --> one DB

Under CQRS:

Client --command--> write model --> write store / event log
|
v
[projections]
|
v
Client --query----> read model(s) <--- read store(s)

The write side enforces invariants; the read side answers queries. They are decoupled in code, often in storage, and always in schema.

CQRS is frequently paired with event sourcing (Concept 13), but the two are independent. You can do CQRS without event sourcing (a write DB + async projections to a read DB), and event sourcing without full CQRS (though the natural gravity pulls toward both).

Why It Matters Here

CQRS is the structural pattern that makes projections (Concept 14) a first-class design element rather than a sideshow. It earns its complexity when:

  • read and write workloads differ dramatically (read-heavy, with many distinct query shapes; write-validated through complex invariants)
  • you want multiple read shapes (search index + analytics cube + dashboard table) over the same source
  • the write model's schema is wrong for reads (normalized vs denormalized; row-per-transaction vs aggregated-per-day)
  • team boundaries split query and command concerns

It is not free. Adding CQRS adds eventual consistency to the read path, extra deployable components, and schema sprawl. Apply selectively.

Concrete Example

Without CQRS

POST /orders       (command)
GET /orders?... (query)
|
v
one Order service, one Postgres DB with `orders` and related tables
all reads and writes go through the same model

This is fine for most systems.

With CQRS (without event sourcing)

Write side writes to the primary DB and publishes events (via outbox, Concept 06). Read side consumes events and materializes read models.

commands -> [OrderService.write] -> orders DB + outbox
|
[broker]
|
+---------------------------------+-----------------------------+
| | |
v v v
[customer_summary_projection] [analytics_cube_projection] [search_index_projection]
| | |
v v v
customer_dashboard_db analytics_warehouse elasticsearch

Benefits:

  • the write model keeps tight invariants in Postgres
  • the customer-facing dashboard has its own denormalized store (no joins at read time)
  • analytics does not affect OLTP
  • search is a separate concern entirely

With CQRS + event sourcing

Same picture, except the write store is the event log itself. The normalized write DB disappears; aggregates are folded from events on command handling (Concept 13), and read models are projections (Concept 14).

This is the "full CQRS+ES" shape. It is powerful and expensive.

What CQRS Gives You

CapabilityWhyPrice
Distinct schemas per read shapeReads are optimized; no one schema to please everyoneSchema sprawl
Independent scalingScale read replicas / projections differently from writesMore moving parts
Team partitioningQuery teams work on read models; command teams on invariantsCoordination overhead
Rebuildable read sidesDrop projection, re-consume, doneEventual consistency
Diverse storesRelational + search + columnar + cacheOperational complexity

What It Does Not Give You

  • Stronger consistency. Quite the opposite; you accept eventual consistency on the read side.
  • Simplicity. CQRS is a specialization move. It trades complexity in the domain model for complexity in the pipeline.
  • Solution to all scaling problems. Many read-scaling issues are solved by caches, materialized views, or read replicas without CQRS.

When CQRS Is Right

  1. Multiple read shapes are genuinely needed. You can name 3+ distinct query workloads over the same data, with materially different shape requirements.
  2. The write side has complex invariants. A rich domain model would be degraded by serving reads through the same surface.
  3. You already publish events (outbox / ES). CQRS is the natural consumer side of that publication.
  4. You need cross-store federation. "Search + ranking + analytics + dashboards" over the same source -- CQRS pays off here.
  5. You have operational maturity. Running eventual consistency, monitoring lag, handling rebuilds -- these are not beginner tasks.

When CQRS Is Wrong

  1. CRUD workloads with one or two simple read queries. Use a DB.
  2. You need read-your-own-write guarantees on every request. Possible with CQRS (serve from write side or wait for projection lag) but awkward.
  3. Small team, short runway. The operational cost outruns the benefit.
  4. You picked CQRS because "microservices should use CQRS." This is not a reason; it is a meme.

Three Decision Scenarios

Scenario A -- Internal admin tool, 10 users, CRUD over ~12 entities

CQRS? No. One Postgres, one Django/Spring/Rails app. Shipping a "command side" and a "query side" for this workload is pure overhead.

Scenario B -- Trading platform: write validation is the product, reads power a search UI, a position dashboard, and a risk analytics pipeline

CQRS? Yes. Three distinct read workloads, strong write invariants, and the team already has event-driven infrastructure. Separate read models per use case justify the complexity.

Scenario C -- Healthcare claims: audit is a regulatory requirement; claim adjudication has complex invariants; downstream reporting, fraud detection, and provider dashboards are all read sides

CQRS (and event sourcing)? Yes. Audit + regulatory + multiple read shapes + ability to rebuild views when rules change. This is the canonical setting for CQRS+ES.

Common Confusion / Misconception

"CQRS means two databases." It can mean many databases, one database with separate read and write schemas, or even one physical store accessed through separate code paths. The separation of models is the commitment; the physical shape is an implementation choice.

"CQRS requires event sourcing." No. CQRS without event sourcing is a common and reasonable middle ground: write to a normalized DB, publish events via outbox, build projections.

"CQRS gives read-after-write consistency." It gives eventual consistency on the read side. Some platforms add mechanisms (read from write-side for your own user, wait on projection lag, etc.) but those are extras.

"CQRS fixes slow queries." Sometimes. Often the right fix is an index, a read replica, or a materialized view. Reach for those first.

"CQRS is microservices." CQRS is a pattern for one bounded context. It is orthogonal to microservices; a modular monolith can do CQRS internally.

How To Use It

Adoption checklist (per bounded context, not per system):

  1. Can you name 3+ distinct read shapes? If not, skip CQRS.
  2. Do you already publish reliable events (outbox / ES)? If not, start there.
  3. Can you tolerate eventual consistency on the read path? If not, decide which specific reads must be strongly consistent.
  4. Can you operate multiple stores (lag monitoring, rebuilds, rescheduling)?
  5. Pick the write model: normalized DB with events, or event sourcing.
  6. Define read models per query. One projection per shape, not one "read side" trying to do everything.
  7. Document the consistency story for each endpoint: "dashboard is ~2s behind; search is ~5s."

Check Yourself

  1. What is the single commitment CQRS makes, and what does it trade for it?
  2. When would you reach for CQRS without event sourcing, and when for CQRS+ES?
  3. Name three scenarios where CQRS is overkill.
  4. Why is "read-after-write consistency" the main UX problem CQRS introduces, and what are two ways to mitigate it?

Mini Drill or Application

For each of these three scenarios, decide CQRS yes/no/partial in 5 minutes each and justify:

  1. A banking system with complex transaction validation, regulatory audit, a customer dashboard, a fraud detection pipeline, and an analytics warehouse.
  2. An internal HR tool with 3 CRUD screens and a reports page.
  3. A marketplace: sellers publish listings (complex validation), buyers search (elasticsearch), analytics needs conversion funnels, and there is a real-time "recently sold" feed.

Compare your answers to the criteria above.

Transfer to Adjacent Domains

  • Projections (Concept 14) and event sourcing (Concept 13). CQRS is the framing; projections are the mechanism; ES is an optional upstream choice. Adopt them in that order only if the evidence says so -- many teams adopt CQRS when projections alone would have been sufficient.
  • Read-your-own-write UX. The single most common CQRS failure mode in consumer apps: "I updated my profile, the dashboard still shows the old one." Mitigations: serve the just-written row from the write side, show an optimistic UI, or wait on projection lag. Pick one explicitly per endpoint.
  • Analytics ↔ OLTP split. CQRS is a disciplined version of "don't run analytics queries on the OLTP DB." If you already have a data warehouse fed from events, you have half of CQRS without naming it.
  • Team topologies (S7M4). CQRS naturally splits "write-model team" (invariants, domain logic) from "read-model teams" (query shapes). This works well if the teams are aligned to bounded contexts and not if they're fragmented across shared aggregates.
  • Performance engineering. CQRS is not a scalability trick. Most read-scale issues are solved by indexes, caches, or read replicas with an order of magnitude less complexity. Reach for CQRS for shape problems, not throughput problems.

Read This Only If Stuck