CQRS (Command Query Responsibility Segregation): use a different model to change data than the one you use to read it, instead of one model that does both. The write side is optimized for enforcing business rules; the read side is optimized for fast, purpose-shaped queries.
Everything else in this document is the consequences and variations of that one idea.
Table of contents#
- TL;DR cheat sheet
- Origins: from CQS to CQRS
- The problem it solves
- Anatomy: the command side and the query side
- CQRS is a spectrum, not a switch
- CQRS vs. Event Sourcing
- Read models and projections
- Consistency and read-your-own-writes
- The dual-write problem and the outbox pattern
- CQRS vs. related concepts
- When to use it — and when not to
- Worked example: e-commerce orders
- Common pitfalls and anti-patterns
- Trade-offs summary
- Interview Q&A
- Glossary
- Further reading
TL;DR cheat sheet#
| Question | Short answer |
|---|---|
| What is it? | Separate models for writes (commands) and reads (queries). |
| Where does the name come from? | Bertrand Meyer's Command-Query Separation, lifted to the architecture level by Greg Young. |
| Does it require two databases? | No. That's the far end of a spectrum. Single-DB CQRS is valid. |
| Does it require event sourcing? | No. They're orthogonal; they just combine well. |
| Does it require eventual consistency? | Only when reads are projected asynchronously to a separate store. |
| Biggest risk? | Over-applying it. Use it in a bounded context, not system-wide. |
| Killer follow-up topics | Read-your-own-writes, the dual-write problem / outbox, projection rebuilds. |
Origins: from CQS to CQRS#
CQRS descends directly from Bertrand Meyer's Command-Query Separation (CQS) principle:
Every method should either be a command that performs an action (changes state, returns nothing), or a query that returns data (no observable side effects) — but not both.
CQS is a method-level guideline: stack.pop() (mutates + returns) violates it; stack.peek() (query) and stack.remove() (command) respect it.
Greg Young took that principle and raised it from the method level to the architectural level. Instead of "a method is either a command or a query," CQRS says "the whole model you use for commands is separate from the whole model you use for queries."
That single move — splitting the model in two — is the entirety of CQRS. It is a small pattern with large downstream consequences.
The problem it solves#
In a traditional CRUD design you have one model (often an ORM entity) that you both mutate and read through. That single model is forced to satisfy two very different masters:
| Concern | The write side wants… | The read side wants… |
|---|---|---|
| Shape | Normalized, minimal duplication | Denormalized, matches the screen/report |
| Priorities | Invariants, validation, transactions | Speed, simple queries, caching |
| Behavior | Rich domain logic | No behavior — just data projection |
| Scaling | Fewer, consistent writes | Many, cheap reads |
Trying to serve both with one model produces the friction every CRUD developer knows:
- DTOs awkwardly hand-projected out of entities.
- N+1 query problems and joins that exist only to reshape data for the UI.
- ORM mappings that grow baroque because the same class is used for persistence, validation, and display.
- Read performance and write correctness fighting over the same schema.
CQRS resolves the tension by refusing the compromise: let each side have the model it actually wants.
Anatomy: the command side and the query side#
The command side#
A command is an instruction to do something, named after intent, not after data:
- ✅
PlaceOrder,CancelReservation,ChangeEmailAddress,ApproveInvoice - ❌
UpdateOrderTable,SetOrderStatusColumn
Key properties of commands:
- They express intent. The name captures a business operation, which makes the system's capabilities self-documenting and enables task-based UIs.
- They can be rejected. A command handler validates business rules and may refuse. Commands are requests, not guaranteed mutations.
- They are handled by a single handler that loads the write model (often a DDD aggregate), enforces invariants, and persists the result.
// A command is a plain, intent-carrying message (an immutable data class)
data class PlaceOrder(
val customerId: UUID,
val items: List<OrderLine>,
)
data class OrderLine(val sku: String, val qty: Int)
// Commands can be accepted or rejected — model the outcome explicitly
sealed interface PlaceOrderResult {
data class Accepted(val orderId: UUID) : PlaceOrderResult
data class Rejected(val reason: String) : PlaceOrderResult
}
// The handler owns the business rules
class PlaceOrderHandler(
private val customers: CustomerRepository,
private val orders: OrderRepository,
) {
fun handle(cmd: PlaceOrder): PlaceOrderResult {
val customer = customers.findById(cmd.customerId).orElseThrow()
val order = Order.place(customer, cmd.items) // invariants live in the aggregate
if (!customer.hasCredit(order.total)) {
return PlaceOrderResult.Rejected("INSUFFICIENT_CREDIT") // commands can fail
}
orders.save(order) // persist to the WRITE model
return PlaceOrderResult.Accepted(order.id)
}
}
The query side#
A query asks a question and returns data shaped for the consumer. It has no side effects and, ideally, no domain objects — often just SQL straight into a flat DTO.
data class OrderSummaryDto(
val orderId: UUID,
val customerName: String,
val total: BigDecimal,
val status: String,
val placedAt: Instant,
)
class GetOrderSummaryHandler(private val jdbc: JdbcClient) {
// Read straight from a read-optimized view; no aggregate, no business logic.
fun handle(orderId: UUID): OrderSummaryDto =
jdbc.sql(
"""
SELECT order_id, customer_name, total, status, placed_at
FROM order_summary_view
WHERE order_id = :orderId
""".trimIndent(),
).param("orderId", orderId)
.query(OrderSummaryDto::class.java)
.single()
}
The dispatcher (bus)#
Commands and queries are frequently dispatched through a thin bus that routes a message to its handler. On the JVM, Axon's CommandGateway / QueryGateway provide exactly this (the pattern was popularized in .NET by MediatR); but it's framework-agnostic — in plain Spring Boot you can autowire the target handler directly:
commandBus.send(PlaceOrder(customerId, items)) // command → exactly one handler
val dto = queryBus.query(GetOrderSummary(orderId)) // query → returns data
The bus is not required for CQRS — it's just a common ergonomic wrapper. The essential part is the separation of models, not any particular plumbing.
CQRS is a spectrum, not a switch#
This is the single most important practical point, and the most common thing people get wrong. CQRS does not require two databases, a message broker, event sourcing, or eventual consistency. Those live at the far end of a range of implementations:
| Level | What's separated | Storage | Consistency | Complexity | Use when… |
|---|---|---|---|---|---|
| 0 — Code split | Command vs. query code paths only | One DB, same tables | Strong | Very low | You just want queries to bypass the domain model and read into DTOs. |
| 1 — Separate read shapes | Separate read structures (denormalized tables, materialized views) | One DB | Strong (updated in the same txn) | Low | Reads need a different shape but you don't want a second store. |
| 2 — Separate stores | Separate read database, updated by a projection | Two stores | Eventual | Medium–High | Reads and writes scale very differently, or you need multiple specialized read stores. |
| 3 — Event-sourced CQRS | Write side stores events; read models are projections of the event stream | Event store + read stores | Eventual | High | You already need an audit log / temporal queries, or the domain is event-centric. |
You can do perfectly good CQRS in a single Postgres database (Level 0 or 1). The real skill is choosing where on this spectrum a given bounded context should sit — and most contexts belong at the low end.
Interview signal: If you say "CQRS means two databases and a message bus," a strong interviewer marks you down. If you describe the spectrum and default to the low end, you signal real experience.
CQRS vs. Event Sourcing#
These are orthogonal and get conflated constantly.
- Event Sourcing (ES): store state as an append-only log of events; the log is the source of truth, and current state is derived by replaying events.
- CQRS: separate the read model from the write model.
You can have any combination:
| Without CQRS | With CQRS | |
|---|---|---|
| Without ES | Classic CRUD | Write to a normalized DB, project to read views |
| With ES | Event log + replay to current state only (rare, awkward to query) | The common, powerful combination |
They pair well because an event log is terrible to query directly — so event sourcing naturally pushes you toward building read models (i.e., toward CQRS). But each adds complexity independently, and Greg Young is emphatic they are separate decisions.
Rule of thumb: Adopt CQRS for the model-shape and scaling benefits. Adopt event sourcing for the audit-log / temporal-query / "why did the state become this?" benefits. Don't assume choosing one forces the other.
Read models and projections#
A read model (a.k.a. projection or materialized view) is a data structure built specifically to answer a class of queries. A projector keeps it in sync by reacting to changes/events on the write side.
Because read models are cheap and disposable, one write side can feed many read models, each optimized for a different consumer (a UI screen, a search index, a reporting rollup, a cache).
Properties every projector needs#
- Idempotency. Events can be redelivered (at-least-once delivery is the norm). Applying the same event twice must not corrupt the read model. Use upserts keyed by entity id, and/or track the last-applied event position.
- Ordering tolerance. Events may arrive out of order across partitions. Either enforce per-entity ordering, or design projections to be commutative where possible.
- Rebuildability. A superpower of this style: if a projection has a bug, fix the code and rebuild the read model from scratch (replay events / re-run the projection) — without touching the source of truth. New read models can be backfilled the same way.
// Idempotent projector: safe to re-run
class OrderSummaryProjector(
private val jdbc: JdbcClient,
private val checkpoints: CheckpointStore,
) {
// or @KafkaListener / @RabbitListener, etc.
fun on(event: DomainEvent) {
when (event) {
is OrderPlaced ->
// upsert, not insert → redelivery-safe
jdbc.sql(
"""
INSERT INTO order_summary_view (order_id, customer_name, total, status, placed_at)
VALUES (:id, :name, :total, 'PLACED', :at)
ON CONFLICT (order_id) DO UPDATE
SET customer_name = excluded.customer_name,
total = excluded.total
""".trimIndent(),
).param("id", event.orderId)
.param("name", event.customerName)
.param("total", event.total)
.param("at", event.at)
.update()
is OrderShipped ->
jdbc.sql("UPDATE order_summary_view SET status = 'SHIPPED' WHERE order_id = :id")
.param("id", event.orderId)
.update()
else -> Unit // ignore events this read model doesn't care about
}
checkpoints.save("order-summary", event.position) // track progress for rebuilds
}
}
Consistency and read-your-own-writes#
At Levels 0–1 of the spectrum the read model is updated in the same transaction as the write, so reads are strongly consistent. The consistency conversation only begins at Level 2+, where a separate read store is updated asynchronously.
Then the read model lags the write model — usually milliseconds, but nonzero. The classic symptom is the read-your-own-writes problem:
Ways to handle it, roughly from simplest to most involved:
- Return the new state from the command result instead of re-querying (the client already knows the outcome).
- Optimistic UI: render the change client-side immediately; reconcile when the read model catches up.
- Version / ETag the read model (expose a sequence number) and have the client poll until it catches up to the write it just made.
- Route the writer's immediate follow-up read to the write store or a synchronous projection for that session.
The honest framing for an interview: eventual consistency is a cost you accept in exchange for independent scaling and flexible read models — not a free feature. If a use case genuinely cannot tolerate any lag, keep it at Level 0–1.
The dual-write problem and the outbox pattern#
Once the read side lives in a separate store or broker, a subtle failure mode appears. Suppose the command handler does:
1. write the state change to the write DB ✅ succeeds
2. publish an event to the broker ❌ fails (or the process crashes here)
Now the write happened but nobody was told — the read model silently drifts forever. This is the dual-write problem: two writes to two systems with no shared transaction cannot be made atomic by ordering alone.
Solution 1 — Transactional Outbox (most common)#
Write the event into an outbox table in the same database transaction as the state change. A separate relay reads the outbox and publishes. Because the state change and the outbox row commit atomically, you can never have one without the other.
Because delivery is at-least-once, the projector must be idempotent (see above) — the two patterns are partners.
Solution 2 — Change Data Capture (CDC)#
Tail the database's transaction log (e.g., with Debezium) and derive events from committed changes. No application-level outbox code; the log is the outbox.
Solution 3 — Event Sourcing sidesteps it entirely#
If the event is the write (event sourcing), there is no second write to keep in sync — appending the event and recording the state change are the same act.
CQRS vs. related concepts#
| Compared with | How it differs |
|---|---|
| Read replicas | A replica is the same schema physically copied for read scaling. A CQRS read model is a different, purpose-built schema per use case. Replicas scale reads; CQRS reshapes them. They're complementary — you can project into read models and replicate each. |
| Plain repository / DTOs | Returning DTOs from a repository isn't CQRS if it still reads through the same write model. CQRS requires the read path to use a genuinely separate model. |
| Microservices | Orthogonal. CQRS is an internal pattern for one service/bounded context. You can use it in a monolith; you can build microservices without it. |
| Database views | A materialized view is one lightweight way to implement a CQRS read model (Level 1). CQRS is the broader architectural intent; the view is a mechanism. |
Interview gold: cleanly distinguishing read replicas (same model, copied) from CQRS read models (different model, projected) signals that you understand the pattern rather than pattern-matching on "reads are separate."
When to use it — and when not to#
Good fits#
- A domain with rich write-side invariants and many divergent read shapes (UI views, search, reporting).
- Asymmetric scaling — reads vastly outnumber writes (or vice-versa) and you want to scale them independently.
- Collaborative, task-based domains where many users act on the same data and intent-named commands model the workflow well.
- You are already doing event sourcing (read models are essentially mandatory there).
Poor fits (reach for plain CRUD instead)#
- Simple CRUD where the read and write shapes are nearly identical — CQRS is pure overhead.
- Applying it system-wide. CQRS belongs to specific bounded contexts, not the whole application. Global CQRS is a classic over-engineering trap.
- A team not ready for the operational cost of eventual consistency (monitoring projection lag, rebuilds, idempotency, the outbox).
The people who popularized CQRS (Martin Fowler, Greg Young, Udi Dahan) all warn that it is overused. Young has quipped that most teams using CQRS probably shouldn't have. In an interview, volunteering the "when not to" is often worth more than reciting the mechanics.
A quick decision heuristic#
Worked example: e-commerce orders#
Write side. PlaceOrder → the Order aggregate validates stock, credit, and business rules → persists the order (or, under event sourcing, appends an OrderPlaced event). One model, tightly guarding invariants.
Read side. Several independent read models, each fed by a projector:
| Read model | Optimized for | Possible store |
|---|---|---|
customer_order_history | "My orders" list, one row per order, per customer | Postgres table |
order_detail_view | Product names & prices as of purchase, live shipping status | Postgres table |
fulfillment_queue | Admin view of orders needing action | Postgres / cache |
order_search | Full-text and faceted search | Elasticsearch |
daily_revenue | Reporting rollup | Data warehouse |
End-to-end flow at Level 2+:
The write model never contorts itself to serve any screen; each read model evolves independently and can be rebuilt from the event history if its logic changes.
Common pitfalls and anti-patterns#
- Treating CQRS as all-or-nothing / global. Apply it to the one or two contexts that need it; leave the rest CRUD.
- Assuming it requires event sourcing or a message bus. It doesn't — start at Level 0–1.
- Ignoring read-your-own-writes. Design the UX for lag before shipping async projections.
- Non-idempotent projectors. At-least-once delivery will redeliver; non-idempotent handlers corrupt read models.
- The naked dual-write. Writing to the DB and publishing an event separately, with no outbox/CDC — guarantees eventual drift.
- No rebuild story. If you can't replay/rebuild a read model, you've lost one of CQRS's biggest advantages and made projection bugs permanent.
- Leaking domain objects into the read side. Queries should return DTOs, not aggregates; otherwise the "separation" is illusory.
- Over-engineering the command side. Not every command needs a full aggregate and event stream; simple commands can be simple.
Trade-offs summary#
| ✅ Benefits | ⚠️ Costs |
|---|---|
| Read and write models each optimized for their job | More moving parts; higher cognitive & operational load |
| Independent scaling of reads vs. writes | Eventual consistency (at Level 2+) and its UX handling |
| Many specialized read models from one write model | Must build & monitor projections, checkpoints, lag |
| Read models are disposable & rebuildable | Idempotency + ordering discipline required |
| Clear, intent-revealing command vocabulary | Dual-write risk → need outbox/CDC |
| Pairs naturally with event sourcing & audit needs | Easy to over-apply and hurt a simple system |
Interview Q&A#
Q: What is CQRS in one sentence? Using a separate model to update information than the model you use to read it, so each side can be optimized independently.
Q: Does CQRS require two databases? No. That's the far end of a spectrum. You can do CQRS in a single database by simply separating the command and query models/code paths.
Q: How is CQRS related to event sourcing? Orthogonal. ES stores state as an event log; CQRS separates read/write models. They combine well because event logs are hard to query (so you build read models), but each is an independent decision.
Q: How do you keep the read model in sync with the write model? Projections driven by change events, delivered reliably via a transactional outbox or CDC, applied by idempotent projectors that track their position for rebuilds.
Q: What's the dual-write problem and how do you solve it? Writing to the DB and separately publishing an event aren't atomic, so one can succeed while the other fails, drifting the read model. Fix with the outbox pattern (event written in the same transaction) or CDC; event sourcing avoids it because the event is the write.
Q: How do you handle read-your-own-writes under eventual consistency? Return the result from the command, use optimistic UI, version/poll the read model until it catches up, or route the writer's immediate read to the write store.
Q: How do you rebuild a read model? Replay the event stream (or re-run the projection over the source) into a fresh read model — possible precisely because read models are derived and disposable.
Q: How is a CQRS read model different from a read replica? A replica is the same schema copied for scale; a CQRS read model is a different, purpose-built schema per query need. Complementary, not the same.
Q: When would you NOT use CQRS? Simple CRUD with matching read/write shapes; and never apply it across the whole system — scope it to bounded contexts that benefit.
Q: Why name commands after intent (PlaceOrder) instead of data (UpdateOrder)?
Intent-named commands document business capabilities, enable task-based UIs, capture why state changed (great for auditing/event sourcing), and let handlers enforce operation-specific rules.
Glossary#
- Command — an intent-carrying message that requests a state change; can be rejected.
- Query — a request for data with no side effects; returns a DTO.
- Aggregate — a cluster of domain objects treated as one consistency boundary on the write side (DDD).
- Read model / Projection / Materialized view — a data structure built to answer a class of queries.
- Projector — the process that updates a read model in response to changes/events.
- Eventual consistency — the read model converges to the write model after a delay.
- Outbox — a table written in the same transaction as a state change, later relayed to a broker, to avoid dual writes.
- CDC (Change Data Capture) — deriving events by tailing the database transaction log.
- Idempotent — applying the same operation/event more than once has the same effect as once.
- Bounded context — a boundary within which a particular model applies (DDD); the right scope for adopting CQRS.
Further reading#
- Greg Young — talks and writings that coined and defined CQRS; and his cautions about overuse.
- Martin Fowler — CQRS bliki entry (clear definition + explicit warnings about when not to use it).
- Udi Dahan — "Clarified CQRS" (context on collaborative domains and common misunderstandings).
- Axon Framework Reference Guide — the canonical CQRS + event-sourcing framework on the JVM (
CommandGateway/QueryGateway,@EventHandlerprojections, event store). - Spring Modulith — Event Publication Registry — an outbox-style mechanism built into Spring Boot: application events are persisted in the publishing transaction and republished if a listener didn't complete.
- Chris Richardson, Microservices Patterns — the Outbox, CDC, and Saga patterns that surround CQRS in distributed systems.
Scope note: apply CQRS to the bounded contexts that genuinely benefit, default to the low end of the spectrum, and treat eventual consistency and the outbox as deliberate costs — not defaults.