CQRS
Splitting the write model from the read model — when and when not, what it costs, and the relationship to event sourcing
CQRS
CQRS — Command Query Responsibility Segregation — is the pattern of using one model to update state and a different model to read state. The two are connected (writes eventually become visible to reads), but they are designed independently: different schemas, different stores, sometimes different services entirely.
Greg Young, who popularized the pattern, has spent a decade clarifying that CQRS is just this separation. It does not require event sourcing, it does not require multiple databases, it does not require asynchronous projections. Those are common companions, not parts of the definition. The minimum CQRS is two interfaces — a write API and a read API — in the same service, sharing the same database. Even that is a real architectural choice.
This page is about when that separation is worth doing, what costs it brings, and how it relates to Event Sourcing and Event-Driven Architecture.
What CQRS Is
The traditional CRUD model has one model for everything:
[Client]──read──▶[Service]──▶[DB: same model, same tables]
[Client]──write──▶[Service]──▶[DB: same model, same tables]CQRS splits this:
[Client]──command──▶[Write side: optimized for writes]──▶[Write DB]
│
(projection)
▼
[Client]──query────▶[Read side: optimized for reads ]◀──[Read DB]The two sides have different shapes:
- Write side — accepts commands, validates them, applies state changes. Optimized for correctness and consistency. May use normalized tables, aggregates, domain logic.
- Read side — accepts queries, returns data. Optimized for query patterns. May use denormalized tables, materialized views, search indices, caches.
The two sides are connected by a projection: a mechanism that propagates write-side changes to the read side. The projection can be synchronous (same transaction) or asynchronous (event-driven, polling, change data capture).
What CQRS Buys You
The benefits are specific:
- Read and write can be optimized independently. The write model can be normalized for consistency; the read model can be denormalized for query speed. Each side scales according to its load.
- Multiple read models for the same data. One write produces several projections: a per-user dashboard view, an analytics view, a search index, a cached aggregate. Each is shaped for its use case.
- Asymmetric scaling. Reads usually dominate writes by 10-100x. Splitting lets you scale the read side without paying for the write side.
- Different stores for different needs. Write to Postgres, read from Elasticsearch (search) plus Redis (cache) plus a denormalized Postgres view (queries). Each store is good at its job; together they cover the workload.
- The write model can stay simple. Without CQRS, the write model is often distorted to support read queries. With CQRS, writes can stay focused on consistency and business rules.
What CQRS Costs
The honest other side:
- Two models to maintain. Writes and reads now diverge. A change to the domain typically requires changes to both sides plus the projection.
- Eventual consistency between write and read. Asynchronous projections mean the user who just wrote does not immediately see the result. The application must surface this — optimistic UI updates, refresh after acknowledgement, or explicit "loading" states.
- More moving parts. A projection process, an event bus or polling mechanism, possibly multiple read stores. Each is a thing that can break.
- Operational complexity. Drift detection between write and read stores. Rebuild procedures for read stores. Replay infrastructure if projections fall behind.
- The "I just wrote that, where is it?" problem. Users notice consistency lag. Read-your-writes (Consistency Models) is much easier when reads and writes share the same store; with CQRS, you have to design for it.
When CQRS Is Worth It
The classic signals:
- Reads and writes have fundamentally different shapes. Writes are by
userId; reads are byproductId. Writes are short transactions; reads are large aggregations. The mismatch makes a single model awkward. - Read load dominates write load by orders of magnitude. A social feed read 100,000 times per write justifies splitting.
- Multiple distinct read views are needed. Operational dashboard, customer-facing display, analytics, search — all from the same writes.
- The domain model is genuinely complex. A rich domain model (aggregates, business rules, invariants) deserves to be expressed cleanly on the write side, free from the contortions needed to support reads.
- You can tolerate the consistency lag. The user experience is designed around eventual consistency, not pretending to be synchronous.
When CQRS Is Not Worth It
Most CRUD applications, frankly. Specifically:
- Simple domains. A user record, a product record, an order record — read by ID, written by ID. Splitting buys nothing.
- Small read/write asymmetry. Reads and writes are within an order of magnitude. The cost of two models is not justified.
- Strong consistency requirements on read-after-write. Hard to provide cheaply across an async projection. Use a single model with the same store.
- Small system, small team. The operational overhead overwhelms the architectural benefit. A single Postgres with the right indexes is the right answer.
- Early-stage products. You do not yet know what your read patterns are. Optimizing for them prematurely freezes the wrong shape.
The honest framing: CQRS is a tool for specific problems, not a default. Most applications should not adopt it.
The Relationship to Event Sourcing
Event sourcing and CQRS are often discussed together because they pair naturally — but they are independent choices.
- CQRS without event sourcing. Common and useful. The write side updates a normalized state in the database; a projection (synchronous or async) populates the read side. The write side is "current state," not events.
- Event sourcing without CQRS. Possible but rare. Events are stored, but a single read model is derived for both queries and the write side's own state checks.
- CQRS with event sourcing. The standard combination in heavyweight CQRS implementations. Events are the truth on the write side; one or more projections build read models from the event stream.
Treating "CQRS" and "event sourcing" as the same thing is a common confusion. Either alone is a real choice with its own trade-offs.
A Worked Example
Consider an e-commerce order system.
Without CQRS (standard CRUD):
CREATE TABLE orders (
id, user_id, status, total, created_at, ...
);
CREATE TABLE order_items (
order_id, sku, qty, price, ...
);
-- Write: INSERT into orders, INSERT into order_items
-- Read: complex JOINs across orders, items, products, usersThe read query is doing serious work. The customer's "my orders" page joins three tables, plus product info, plus pricing. Caching helps but each cache invalidation reloads from this complex query.
With CQRS:
-- Write side: normalized
CREATE TABLE orders (id, user_id, status, total, ...);
CREATE TABLE order_items (order_id, sku, qty, price, ...);
-- Write commands: validated, transactional, focused on consistency
-- Read side: denormalized "my orders" view
CREATE TABLE user_order_view (
user_id,
order_id,
order_status,
product_names_json,
total,
primary_image_url,
formatted_date,
...
);After every order write, a projection updates user_order_view with the denormalized representation. The read query is now a single-table lookup by user_id. The write side is unchanged from a domain-modeling perspective.
The cost: an extra table, a projection process, eventual consistency between the two, and the discipline to update the projection logic when the read shape needs to change.
Common Mistakes
- Adopting CQRS for every domain entity. CQRS is a per-aggregate choice, not a system-wide one. Most entities should remain CRUD; CQRS goes where it earns its cost.
- Conflating CQRS with event sourcing. Adopting both at once because they are often mentioned together. Each has its own complexity; do not pay both unless both are justified.
- Ignoring read-your-writes. Users notice. Either accept the lag and design the UX for it, or use a stronger consistency guarantee for the path that matters.
- Letting the read model diverge from the write model semantically. "The read says X but the write says Y" is a bug, not a feature. Projections must be correct.
- Not having a rebuild procedure for read stores. Read stores get corrupted (bug in projection, partial data import). Without a procedure to rebuild from the write side, you are stuck.
- Over-projecting. A new read view for every query pattern. Each is a thing to maintain. Add them deliberately.
Further Reading
- Greg Young, CQRS, Task Based UIs, Event Sourcing agh! (2010) — the originating essay; Greg has been clarifying it ever since.
- Greg Young, CQRS Documents (2010) — the longer writeup that became the reference document for years.
- Martin Fowler, CQRS (2011) — the most accessible introduction.
- Udi Dahan, Clarified CQRS — argues that CQRS makes sense only for collaborative domains; useful counterpoint.
- Vaughn Vernon, Implementing Domain-Driven Design — Chapter 4 covers CQRS in a DDD context.
- Microsoft, CQRS Journey — an early book-length practitioner account; flawed in places but instructive.
Pre-commit Checklist
- For each aggregate I am applying CQRS to, can I name a specific signal (read/write asymmetry, multiple read views, complex domain) that justifies the cost?
- Have I designed for the read-your-writes case, or am I assuming users will not notice consistency lag?
- Is there a rebuild procedure for each read store, in case it gets corrupted or falls behind?
- Are the read models semantically consistent with the write model, or have they drifted?
- Am I conflating CQRS with event sourcing? They are separate choices; justify each independently.
- For aggregates that are simple CRUD, am I leaving them alone — not applying CQRS just because the rest of the system uses it?