System Design

CQRS — Command Query Responsibility Segregation

One model that both writes and reads is two jobs wearing one coat. CQRS takes the coat off.

distributed-systems cqrs data-modeling
CQRS architecture poster: command write path on the left, query read path on the right, joined by an eventually-consistent projection
Visual style: technical-poster · generated for this card

Core Concept

CQRS splits the single data model that normally serves both writes and reads into two independent models: a command model that changes state and returns nothing, and a query model that returns data and changes nothing.

The insight is that these two jobs have opposite requirements. Writes need invariants, validation, transactions, and a normalized shape. Reads need speed, denormalization, and exactly the shape the screen wants. Forcing both through one model means every design decision is a compromise neither side asked for.

CQRS is not "CRUD with extra steps," and it does not require Event Sourcing. It is one decision: stop making one model answer two different questions.

Key Components

1

Command side

Accepts intent — PlaceOrder, CancelBooking. A handler loads the aggregate, enforces business rules, and persists. Returns success or failure, never a payload for display.

2

Query side

Thin handlers over denormalized read models or materialized views. No business logic, no aggregates — just fetch the pre-shaped DTO the caller needs and return it.

3

Projection / sync

The bridge. Changes on the write side propagate to read models via events, a change feed, or replication. This is where eventual consistency enters the system.

4

Independent scaling

Because the paths no longer share a model, each can scale, cache, and be stored differently — often a relational write store beside document or search read stores.

How It Works

  1. A command arrivesThe client sends intent, not rows. PlaceOrder(customer, items) — a verb with a name from the business, not an UPDATE statement.
  2. The write model validatesThe command handler loads the aggregate, checks invariants (stock available, card valid, order not already placed), and rejects the command if any rule fails.
  3. State is persistedThe change is committed to the write store in one transaction. This is the moment of truth: after this, the fact happened.
  4. A projection updates the read modelAn event or change feed carries the fact to the read side, which writes a denormalized row shaped for a specific screen — order summary, customer history, dashboard tile.
  5. Queries read the projectionThe query handler reads that pre-joined row directly. No aggregates loaded, no joins at request time, no business logic executed.
  6. You pay in lagBetween steps 3 and 4 the read model is stale. Usually milliseconds. Your design must decide what the user sees in that window.

Command path

  • Normalized, invariant-enforcing
  • Transactional, strongly consistent
  • Returns void / ack only
  • Optimized for correctness
  • Usually low volume

Query path

  • Denormalized, per-view shaped
  • Eventually consistent
  • Returns DTOs, no side effects
  • Optimized for latency
  • Usually high volume

Real-World Applications

E-commerce

Checkout enforces stock and pricing invariants on the write side, while product pages and order history are served from denormalized read models that survive traffic spikes.

Banking

Transfers are commands guarded by strict balance invariants; statements, running balances, and spend analytics are projections rebuilt from the ledger.

Collaboration

Issue trackers and docs apps accept small writes (assign, comment, edit) but serve heavy filtered boards and search from separate indexes such as Elasticsearch.

Analytics

Operational writes land in a transactional store; dashboards read pre-aggregated tables so no report query ever competes with a customer's checkout.

Microservices

A service owns its write model and publishes events; other services build their own local read models instead of querying across service boundaries.

When NOT to use it: a simple CRUD admin panel with balanced read/write load and no scaling pressure. CQRS costs you two models, a projection to maintain, and a class of staleness bugs. Buy it when the read and write requirements have genuinely diverged — not before.

Checkpoint — answer before you move on

  1. A command changes state and returns no data; a query returns data and changes nothing. What concretely breaks when a single endpoint does both — and why does that make caching and retrying harder?

    Hint: think about what a client can safely retry, and what a proxy can safely cache.
  2. Your read model lags the write model by 200 ms. Name one user-visible symptom, and one way to hide it without making the read path synchronous.

    Hint: the user who just wrote is the one most likely to notice. What could you show them instead of a fresh query?
  3. CQRS and Event Sourcing are almost always mentioned together. Which one can you adopt without the other, and what specifically do you give up by adopting only that one?

    Hint: one is about separating models; the other is about how state is stored. Only one implies the other.