skip to content

What problem does CQRS (Command Query Responsibility Segregation) solve, what does it not require, and when is it not worth adopting?

level: seniorimportance: should knowfreq 52%

answer

  1. Two models: write enforces, read projects
  2. Not event sourcing, not two DBs required
  3. Async propagation ⇒ projection lag
  4. Must be able to rebuild projections
  5. Outbox, never dual writes

basics

~20 s

CQRS splits the write side (commands that change state) from the read side (queries), letting each use its own model and store. It helps when reads and writes have very different shapes or volumes. It does not require event sourcing, and for ordinary CRUD it is pure overhead.

solid answer

~60 s

CQRS separates the model that handles state changes from the model that serves reads. The write model enforces invariants over a normalized, behavior-rich structure; the read model is denormalized and shaped exactly for the screens or APIs that consume it, often in a different store (a search index, a cache, a document store, read replicas). It pays off when: read and write load differ by orders of magnitude and must scale independently; the query shape is a poor fit for the transactional model (heavy joins, aggregations, full-text search, many bespoke views); or complex domain invariants make the write model deliberately awkward to query. Crucially, CQRS does **not** require event sourcing, a message broker, eventual consistency, or two databases — those are common companions, not prerequisites; separate models over one database is legitimate CQRS. When you do split stores, projection lag makes reads eventually consistent, and you inherit projection code, rebuild tooling, and dual-write or outbox concerns. Apply it to the one or two bounded contexts whose drivers justify it — never system-wide by default.

code

pseudocode · 16 lines
pseudocode
// Write side: enforces invariants, returns no data
handle(PlaceOrder cmd):
    order = Order.create(cmd.customerId, cmd.items)   // invariants live here
    orderRepository.save(order)                        // normalized store
    outbox.append(OrderPlaced(order.id, order.total, order.customerId))
    // same transaction as save() -> no dual-write loss

// Projector: turns events into a read-optimized view
on OrderPlaced e:
    orderSummaryView.upsert(                          // denormalized, no joins
        id = e.orderId, customer = e.customerId,
        total = e.total, status = "PLACED")

// Read side: never touches the domain model
query(GetOrderSummary q):
    return orderSummaryView.findById(q.orderId)       // one row, one read

go deeper

for a junior

Say it means handling writes and reads with separate models so each can be shaped for its job, and note that reads may lag slightly if they are updated asynchronously.

for a middle

Name concrete drivers (read/write ratio, query shape mismatch, specialized search), and state that event sourcing and two databases are optional, not required.

for a senior

Discuss propagation options and their consistency implications, projection lag mitigation, rebuild tooling, the outbox pattern, and applying it per bounded context rather than globally.

for a principal

Weigh lifetime cost — double migration surface, operational monitoring, team capability — against measured benefit; define the trigger conditions for adopting or reverting it and how you'd keep the read contract stable while projections evolve.

### Starting from the underlying principle **Command–Query Separation (CQS)**, from Bertrand Meyer, is a method-level rule: a method should either change state (a command, returning nothing) or return data (a query, causing no side effects) — never both. It makes code easier to reason about. **CQRS** lifts that idea to the architectural level: instead of one model serving both purposes, you have *two models* — a write model and a read model — that may differ in shape, in storage, and in scaling. ### What each side looks like **Write (command) side.** Receives commands (`PlaceOrder`, `CancelSubscription`), loads the consistency boundary (in DDD terms, an aggregate), enforces invariants, persists, and typically emits an event. It is normalized and behavior-first; it is intentionally bad at answering "show me all orders from last quarter grouped by region". **Read (query) side.** Serves **projections** — precomputed, denormalized views shaped exactly like the consumer needs, with no domain behavior. "Order summary for the account page" is one table or document with everything the page shows and zero joins. Storage can be anything that fits the query: read replicas, a document store, a search index, a cache, a columnar warehouse. **Synchronization.** How the read side learns of changes: 1. *Same transaction, same database* — write both models atomically. Simple, strongly consistent, no lag; least flexible. 2. *Events/projections* — the write side publishes events; projectors update read stores. Flexible and independently scalable; introduces lag and projection code. 3. *Change data capture* — read the database log and project from it. No producer changes, but couples projections to the physical schema. With options 2 and 3, reads are **eventually consistent**: after a write returns, the read model may lag by milliseconds to seconds. ### The myths to kill - **"CQRS requires event sourcing."** No. Event sourcing (persisting state as an append-only sequence of events) pairs naturally with CQRS because you need projections anyway, but CQRS is perfectly valid over a conventional row-updating store. Greg Young, who named CQRS, has repeatedly pushed back on the conflation. - **"CQRS means two databases."** No. Two *models* is the requirement. Separate command and query services over one schema is a legitimate and cheap first step. - **"CQRS means eventual consistency."** Only if you choose asynchronous propagation. Updating both models in one transaction keeps reads strongly consistent. - **"CQRS means microservices / a broker."** Unrelated. It applies inside a single monolith module just as well. - **"CQRS is just having separate DTOs for reads."** Returning a different DTO from the same model is good hygiene but not architecturally significant. CQRS becomes real when the read path stops going through the domain model at all. ### When it earns its cost - **Asymmetric load** — e.g. a public catalog with a huge read:write ratio, where reads must scale (and be cached/replicated) independently of a comparatively tiny write path. - **Shape mismatch** — the transactional schema requires punishing joins or aggregations for the main screens; a projection turns a 9-table join into one row read. - **Specialized query technology** — full-text search, geospatial, graph traversal, or analytics that the transactional store handles badly. - **Rich domain invariants** — the aggregate is designed for correctness, and forcing it to also serve arbitrary reporting queries corrupts the model. - **Different availability profiles** — reads should keep working (possibly stale) while writes are degraded or under maintenance. ### When it is not worth it - Ordinary CRUD with a shared model — you double the code for no measurable gain. - The team is not yet comfortable operating asynchronous propagation, monitoring lag, and rebuilding projections. - Consistency requirements make lag unacceptable and you would end up reading from the write model anyway. - You have no measured problem. Adopting CQRS "for scale" pre-emptively is the same error as premature microservices. ### Operational realities once adopted - **Projection lag** must be measured and alerted on; users notice a stale list right after an edit. Mitigate with optimistic UI updates, versioned reads ("wait for version ≥ N"), or reading the write model for the just-edited entity. - **Rebuild capability** is mandatory: projections have bugs, and you must be able to replay from the source of truth into a fresh projection and switch over. - **Dual writes are a trap.** Writing to the database and publishing to a broker as two separate operations loses messages when one fails. Use the **transactional outbox** (write the event to a table in the same transaction, relay it afterward) or change data capture. - **Versioning two models** doubles the migration surface — a schema change may require both a write-model migration and a projection rebuild. ### How to answer Define both sides, state clearly the drivers that justify it, dismantle the event-sourcing myth, and describe the operational obligations (lag monitoring, rebuilds, outbox). Emphasize applying it per bounded context, at the smallest scope that solves the measured problem.

  • How do you handle a user who saves an edit and immediately sees the old value because the projection has not caught up?
    Common mitigations: update the UI optimistically from the command result; return the new version/sequence number and have the read path wait for or assert that version; route reads for the just-modified entity to the write model briefly; or push the projected update over websockets/SSE. Pick per screen — a global fix is usually not needed.
  • Why is the transactional outbox pattern important in a CQRS system?
    Because writing to the database and publishing an event are two separate operations that can partially fail, silently losing events and leaving projections permanently wrong. The outbox writes the event into a table inside the same database transaction as the state change; a relay process then publishes it at-least-once, so the state and the event can never diverge.
  • Would you apply CQRS to an entire system?
    No. Apply it per bounded context where the drivers exist — typically one or two contexts with asymmetric load or an awful query shape. System-wide adoption imposes projection code, lag, and rebuild tooling on contexts that are plain CRUD and gain nothing.

A restaurant kitchen versus its menu. The kitchen is organized for making food correctly — stations, prep order, safety rules. The menu is organized for the diner: grouped, described, priced, instantly scannable. Nobody wants diners reading the prep schedule, and nobody cooks from the menu layout. Same food, two purpose-built representations.

context