Skip to main content

CQRS Pattern

Command Query Responsibility Segregation — separate read and write models for optimized performance and scalability.

//The Contract

Requirements

Functionalwhat the system must do
  • Customers can submit commands — place order, update shipping address, cancel order — that mutate state through a single validated write path
  • Customers and internal dashboards can query order status, history, and aggregated analytics without ever touching the transactional write store
  • New read-optimized views (a search index, an analytics rollup) can be added later without changing how commands are validated or written
  • The read store can be dropped and rebuilt from the write store's history if it becomes corrupted or needs a new shape
  • Support staff see a highly available view of order state even during write-side incidents, even if that view is briefly stale
Non-functionalhow well it must do it
  • Strong consistency on writes — an order is never left partially applied; every command either fully commits or fully fails
  • Read availability and latency are decoupled from write load — a checkout traffic spike must not slow down dashboard queries
  • Read replication lag stays bounded — under a few hundred milliseconds at p99 under normal load
  • Read throughput scales an order of magnitude beyond write throughput without touching the write database
  • The system tolerates a temporarily stale read store rather than sacrificing write-side availability
//Back of the Envelope

Scale Estimates

Rough, order-of-magnitude numbers. The point is to justify the architecture, not to be exact.

Write throughput
5K ops/sec
orders placed/updated at peak checkout
Read throughput
250K qps
dashboards + storefront reads dwarf writes
Read : write ratio
50 : 1
justifies a denormalized, independently-scaled read store
Replication lag (p99)
<300ms
the budget that keeps 'eventual' consistency acceptable
Read model size
~10x write model
denormalization duplicates data across query shapes
//Follow the Data

Request Flows

A diagram shows what exists; a trace shows what happens. Press play on a flow, or click any step, and the diagram above lights up the exact path that request takes.

Flow 01

Place an order (write path)

From a validated command to a durable, committed state change.

Flow 02

Query order status (read path)

How a read is served without ever touching the write database.

Flow 03

Rebuild the read model from scratch

What happens when the read side needs a new shape, or recovers from corruption.

//Component Breakdown

Key Components

Every box on the canvas, and the job it does. Click a card to locate it in the diagram; click the node itself for the full deep dive.

client×1
compute×3
database×2
cache×1
//Deep Dive

Architecture Breakdown

Why Split Reads From Writes

Most systems read far more than they write — order status gets checked constantly, orders get placed comparatively rarely. CQRS (Command Query Responsibility Segregation) takes that asymmetry seriously by giving reads and writes two entirely separate models, each free to be optimized for its own workload instead of compromising on one shared schema.

A command mutates state and enforces business rules. A query only reads and is never allowed to change anything. Once that split is explicit, the two paths can scale, fail, and evolve completely independently.

The Write Side

The Command API is the only thing allowed to write. It validates every command against the domain's rules and commits the result to write-db inside a transaction — normalized, correct, and boring on purpose. Boring is the point: the write side's only job is to never let bad data through.

Keeping the Read Side Fresh

write-db never talks to read-db directly. Instead, a change feed carries every committed write to the projector (syncer), which transforms it into whatever shape the read side needs and upserts it into read-db.

This makes the read model:

  • Eventually consistent — a write becomes visible on the read side milliseconds to seconds later, not instantly
  • Disposable — replaying the change feed from the beginning regenerates read-db from scratch
  • Independently shaped — a new query pattern just needs a new projection, not a migration on the write schema

The Read Side

The Query API adds one more optimization on top of the already-denormalized read-db: a cache-aside Redis layer. Because reads outnumber writes 50 to 1, absorbing repeat reads in Redis is often more impactful than anything that could be done to read-db itself.

The Trade-off

CQRS trades strict consistency for independent scalability. A client that writes and then immediately reads its own write may briefly see stale data. That is an explicit, bounded cost — not a bug — and it buys a read path that can be cached, denormalized, and scaled without ever putting the write path's correctness at risk.

//Trade-offs

Design Decisions

An architecture is a record of trade-offs. For every major choice here: what won, what lost, and why the constraints made it so.

01

Separate models for reads and writes

ChoseIndependent write-db and read-db, kept in sync asynchronously
OverSingle normalized database serving both paths

A 50:1 read:write ratio means the two paths have almost nothing in common operationally. Forcing them to share one schema means every index that helps reads slows writes, and every write-optimized constraint forces a join at read time. Splitting the models lets each be tuned — and scaled — for what it actually does.

02

Async propagation instead of dual writes

ChoseChange-feed → projector (syncer) → read-db
OverWrite to both stores in the same transactionTwo-phase commit across write-db and read-db

A distributed transaction across two different databases couples their availability — a slow read-db would now block every write. Streaming changes through a projector keeps write latency dependent on write-db alone, at the cost of read-db lagging by up to the ~300ms p99 budget. That trade is worth it because nothing here needs read-your-write guarantees at the database layer.

03

Cache in front of the read store

ChoseCache-aside Redis layer for the Query API
OverServe every read straight from read-db

At 250K qps, hitting read-db for every request would require a database cluster sized for peak read traffic even though most of those reads repeat the same handful of hot orders. A cache-aside layer absorbs the repeat traffic cheaply, leaving read-db sized for genuine cache misses instead of total volume.

04

Denormalized, query-shaped read model

Choseread-db stores pre-joined, query-optimized documents
OverReuse the normalized write schema for reads too

The write schema is shaped to make single-order updates safe and atomic, not to answer 'show me every order for this customer in the last 90 days' quickly. Denormalizing lets each read model pre-compute exactly the shape its query needs, so adding a new dashboard is a new projection, never a migration on write-db.

//What Breaks First

Bottlenecks & Failure Modes

Reading a system means sensing where it cracks under 10× load. These are the pressure points of this design, and how it holds.

Risk 01

Stale reads right after a write

Failure mode

A customer places an order, then immediately checks its status and — because the projector hasn't caught up yet — doesn't see it. This is the defining failure mode of CQRS, not an edge case.

Mitigation

Bound and monitor replication lag as a first-class SLO (the ~300ms p99 target). For flows that truly need read-your-writes, route that specific read to write-db instead of read-db, or have the client optimistically render its own command's result.

Risk 02

Projector falling behind under write bursts

Failure mode

A sustained spike in commands can outrun the projector's processing rate, growing the change-feed backlog and pushing replication lag well past its budget.

Mitigation

Scale the projector horizontally by partitioning the change feed, and alert on lag directly rather than inferring it — lag is the single metric that tells you the read side is falling behind reality.

Risk 03

Cache and read-db disagreeing

Failure mode

read-cache only refreshes on a miss or explicit invalidation, so it can keep serving an order's old status well after read-db (and write-db) have already moved on.

Mitigation

Invalidate cache entries from the same change feed the projector consumes, and keep a short TTL as a backstop so a missed invalidation self-heals within seconds.

Risk 04

Read-model schema drift

Failure mode

As new query shapes get added, each one is its own projection that can silently fall out of sync with write-side changes — a broken projector can corrupt a read shape for hours before anyone notices a wrong number on a dashboard.

Mitigation

Version each read projection, treat 'replay from scratch' (see the rebuild flow) as a routine operational tool rather than a last resort, and alert on projector error rate, not just lag.

//Active Recall

Test Yourself

If you can answer these without scrolling back up, the architecture is yours. Try each one out loud before revealing.

//Go Deeper

Further Reading

//A Word

From the Creator

Every great system starts as a sketch on a whiteboard. The ability to zoom out and see the whole picture (how services connect, where data flows, what breaks and why) is what separates engineers who build features from engineers who build systems. This diagram is more than boxes and arrows. It's a map of decisions, trade-offs, and intentional design.

Studying architectures isn't just about passing interviews. It's about training your intuition. The more systems you take apart, the better you get at sensing where a monolith will crack, where a queue belongs, or when a cache is hiding a deeper problem. You start seeing patterns instead of chaos.

So keep reading, keep tracing those edges, keep asking "why this way and not that way." The engineers who truly understand large systems are the ones who never stop being curious about how things fit together. That curiosity is the only ingredient that really matters.

Atharva Arbat@arbat_atharva

//Explore

More Patterns