Back to the map

Module 11 · Architectural patterns · Lesson 8 of 9

CQRS

Separate the model that handles writes from the models that serve reads. Why one schema cannot be ideal for both, read models as materialised views fed by events, the eventual consistency that follows and how to hide it.

DeepShould-knowSenior 12 minPro Updated Nov 2026

Why it matters

A normalised schema that keeps writes correct makes the dashboard a twelve-table join; a denormalised table that makes the dashboard instant makes every write update six copies. The two needs conflict in one model. CQRS resolves the conflict by having two: a write model built for correctness and read models built for each screen. It is how high-traffic systems serve reads at scale without compromising the integrity of writes, and it brings the Scaling stage's materialised views to the level of architecture.

The idea

Command Query Responsibility Segregation splits the system's operations into commands, which change state and return nothing but success or failure, and queries, which return data and change nothing. Each side gets its own model. The write model enforces business rules and invariants, usually normalised, usually transactional. Read models (projections) are shaped exactly for a query: a denormalised table per screen, a search index, a cache, a document. Changes flow from the write side to the read side through events.

CommandQueryWrite modelrules, transactionsDBeventsprojectionsread modelsper-screen tables, search, cacheread models are rebuildable from events; staleness is milliseconds to seconds
Commands hit the write model, which publishes events. Projections build read models shaped for each query. Reads never touch the write model.

Why two models

Write model wantsRead model wants
Normalisation so each fact exists once and invariants holdDenormalisation so a screen is one row or one document
Transactions and locks around the aggregateNo locks; many concurrent readers
Shape of the domainShape of the UI or the report
One store with strong consistencyWhatever store fits the query: SQL, search engine, cache, graph
Scaled vertically or by aggregateScaled horizontally with replicas and caches

Trying to satisfy both in one schema yields a compromise that serves neither; separating them lets each be optimal and lets reads scale independently of writes, which in most systems means scaling the part that is 90 percent of the traffic.

Building read models

A projection consumes the write side's events (through an outbox, as the transactions module described) and applies them to its read store: on OrderPlaced insert a row into the orders-by-customer table and increment the customer's order count; on OrderShipped update the status column. Projections are idempotent (events may be redelivered), rebuildable (replay the event history into an empty store to create a new read model or fix a bug) and independent (a slow search indexer does not delay the dashboard table). This is exactly the materialised-view idea at the architectural level, and the Event sourcing lesson shows how it works when the events are the source of truth.

Living with eventual consistency

Read models lag the write model by the event pipeline's latency. Users who submit a form and immediately see a list without their change will file bugs. Standard mitigations: return the result of the command and update the UI optimistically; include the write's version in the response and have the read side wait until its projection has reached that version (read-your-writes); or serve the user's own just-written data from the write model for a few seconds. Say the staleness out loud in the design; it is a contract.

Degrees of CQRS

Not every system needs separate stores. The lightest form is separate code paths: command handlers that load aggregates and enforce rules, query handlers that run optimised SQL against the same database, perhaps against read replicas. The middle form adds dedicated read tables maintained by triggers or events in the same database. The full form uses different stores per read model fed by events. Choose the lightest form that meets the read-scale and shape requirements.

When not to

Simple CRUD with modest traffic gains nothing from two models and pays the synchronisation cost. Screens that must reflect writes instantly and exactly (a bank balance after a transfer) should read from the write model. Teams without event infrastructure should start with separate query paths against replicas before building projections. CQRS is often introduced alongside event sourcing, but they are independent: CQRS works with a plain relational write model.

CQRS · Backplane