CQRS
Segregates write operations (Commands — change state, return no data) from read operations (Queries — don't change state, return data), letting each side evolve, scale and be optimized independently.
Intent
Separate the model that changes the system's state from the model that reads that state. CQRS (Command Query Responsibility Segregation) is the architectural application of Bertrand Meyer's CQS principle — which forbids a single method from doing both at once — raising that separation to the level of distinct models, distinct handlers and, in its more advanced degrees, distinct databases.
A Command expresses an intent to change state: CreateOrder, CancelReservation, UpdateInventory. It's processed by a CommandHandler that validates business rules, mutates state and returns only a confirmation or nothing. A Query expresses a question: ListOrdersByCustomer, GetAccountBalance. It's answered by a QueryHandler that reads the data and returns it without ever changing state.
This separation resolves a fundamental friction: the ideal model for writing data (with protected domain invariants, aggregates with transactional consistency, rich validation) tends to be a poor model for reading data (which often requires joins across multiple entities, use-case-specific projections and display-optimized formats). With CQRS, each side can be modeled the way that best fits its function.
Problem
In systems with a single shared read/write model, conflicts arise that grow over time:
- Read vs. write impedance: a domain model with encapsulated aggregates and protected invariants is excellent for guaranteeing write consistency, but poor for queries that need denormalized, calculated or variously projected data. The result is complex queries with deep joins over a model that wasn't designed to be queried.
- Asymmetric scalability: most systems have read load far greater than write load. Scaling a single model for both cases forces a suboptimal compromise: either the write model is sacrificed to optimize reads, or reads stay slow to preserve write guarantees.
- Queries that expose internal domain details: to serve reports and dashboards, the domain is often forced to expose internal structures that aren't part of any business use case — violating the aggregate's encapsulation.
- Contention on hot data: frequent reads and writes on the same records generate lock contention in the database, degrading performance on both sides simultaneously.
Structure
CQRS's central flow completely separates a Command's path from a Query's path. Both enter through the same entry point (e.g., an HTTP controller), but go through distinct handlers and models.
┌───────────────────────────────────────────────────────────────────────┐
│ ENTRY POINT (Controller / API) │
└──────────────────┬──────────────────────────────┬─────────────────────┘
│ Command │ Query
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ CommandHandler │ │ QueryHandler │
│ │ │ │
│- validates rules │ │- reads data │
│- mutates state │ │- projects DTO │
│- emits events │ │- never mutates state │
└──────────┬──────────┘ └────────────┬────────────┘
│ persists │ reads from
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ Write Model │ │ Read Model │
│ │ │ │
│Rich aggregates │ │Flat projections │
│invariants │ │denormalized │
│ACID transactions │ │optimized per query │
└─────────────────────┘ └─────────────────────────┘
Degrees of separation:
Simple — same database, two models in code (separate ORM mapping per side)
Full — separate databases; Read Model synced via events or polling
Maximum — CQRS + Event Sourcing; Read Model derived from the event stream
The three degrees of adoption
- Simple (same database): the Write Model uses the full ORM with rich aggregates; the Read Model uses direct SQL queries or materialized views in the same database. It's the most common entry point and brings much of the benefit with little added complexity.
- Full (separate databases): the Write Model persists to a relational or document-oriented database; the Read Model is kept in a database optimized for reading (e.g., Elasticsearch, Redis, a read replica, a projection table). Every domain event or state change triggers an update to the Read Model — introducing eventual consistency.
- Maximum (CQRS + Event Sourcing): the Write Model persists immutable events instead of the current state. The Read Model is derived entirely from those events. The current state is reconstructed from the event stream. CQRS and Event Sourcing are orthogonal — one can exist without the other — but combine naturally here.
How it works
The snippet below shows the minimal structure of a CommandHandler and a QueryHandler. Note that the Command doesn't return domain data — only a confirmation identifier or nothing. The QueryHandler never touches the Write Model.
// ── COMMAND: expresses an intent to change state ────────────
interface CreateOrderCommand {
readonly customerId: string;
readonly items: { productId: string; quantity: number }[];
}
// ── COMMAND HANDLER: validates, mutates, persists ────────────
class CreateOrderHandler {
constructor(private readonly repo: OrderRepository) {}
async handle(cmd: CreateOrderCommand): Promise<{ orderId: string }> {
const order = Order.create(cmd.customerId, cmd.items); // business rules
await this.repo.save(order);
return { orderId: order.id }; // just the ID — no read projection
}
}
// ── QUERY: asks the system a question ────────────────────────
interface ListOrdersQuery {
readonly customerId: string;
readonly page: number;
}
// ── QUERY HANDLER: reads, projects, never mutates ────────────
class ListOrdersHandler {
constructor(private readonly db: ReadDatabase) {}
async handle(query: ListOrdersQuery): Promise<OrderSummaryDTO[]> {
// direct SQL, a view, or an optimized index — bypassing the aggregate
return this.db.query(
'SELECT id, status, total FROM orders_view WHERE customer_id = $1 LIMIT 20 OFFSET $2',
[query.customerId, query.page * 20],
);
}
}
The separation is especially visible in the return type: the Command handler returns only correlation data (the generated ID); the Query handler returns a read DTO assembled by the projection, without exposing any domain aggregate.
When to use
- Read load far greater than write load: separating the models lets you scale the read side independently — database replicas, dedicated caches, specialized indexes — without affecting the write side's consistency guarantees.
- A rich domain with complex invariants: when the write model needs to protect serious business rules (credit limits, non-negative inventory, valid state transitions), keeping that model separate from queries prevents read optimizations from introducing gaps in the invariants.
- Queries with formats very different from the domain model: dashboards, reports and listing screens often need denormalized projections that don't exist in the domain model. With CQRS, the Read Model can be designed exactly for each read use case.
- Systems that already use Event Sourcing: the event stream produced by the Write Model is the natural source for feeding specialized read projections — full CQRS emerges almost naturally.
When to avoid
- Simple CRUDs without domain rules: when there are no invariants to protect and no complex queries, CQRS adds layers of indirection with no real benefit. A simple Layered Architecture or a generic repository solves the problem with much less code.
- Small teams without experience with eventual consistency: the full degree (separate databases) requires dealing with stale Read Models, synchronization strategies and compensation logic. The cost of understanding and operating that model is high.
- When immediate consistency is mandatory: if the UI needs to show exactly the state right after every mutation, the Read Model's eventual consistency requires workarounds (polling, optimistic UI, returning state in the Command response) that reduce the value of the separation.
Pros and cons
Pros
- Models optimized for their function: the Write Model protects invariants; the Read Model delivers efficient projections.
- Asymmetric scalability: reads and writes scale independently, with infrastructure matched to each load.
- Frictionless queries against the domain: the Read Model can use direct SQL, materialized views or specialized indexes without compromising domain modeling.
- Natural auditing and traceability: Commands carry explicit intent and can be logged or persisted as a change log.
- Natural combination with Event Sourcing: the Write Model's event stream feeds Read Model projections in a decoupled way.
Cons
- More code and more concepts: two models, two sets of handlers, possibly two databases — the volume of code and the number of concepts grow significantly.
- Eventual consistency (in the full degree): the Read Model can be stale right after a mutation. This requires the UI and the business to accept windows of inconsistency.
- Read Model synchronization: keeping the Read Model updated requires an explicit strategy — domain events, change data capture or polling — that needs monitoring and operation.
- More complex debugging: tracing why a Query returned unexpected data requires checking both the Write Model's state and the Read Model's state, plus the sync mechanism between them.
Common pitfalls
1. Confusing CQRS with Event Sourcing
CQRS and Event Sourcing are orthogonal concepts. CQRS separates read and write models — the Write Model can persist the current state, like any conventional system. Event Sourcing is a persistence strategy where state is derived from an immutable sequence of events. The two combine well, but neither implies the other. Applying CQRS without Event Sourcing is fully valid and much simpler. Applying Event Sourcing without CQRS is equally possible (and equally rare to do well).
Rule of thumb: start with simple CQRS (same database, two models in code). Only move to separate databases or Event Sourcing once you have a measurable problem that justifies the added complexity.
2. Stale Read Model causing subtle bugs
In the full degree, there's a window of time between the Write Model's persistence and the Read Model's update. If the UI runs a Query immediately after a Command and displays the result, the user might see the previous state. This isn't a CQRS bug — it's a property of the model — but it needs to be communicated clearly to the product and handled in the UI. Common solutions include returning the ID and a summarized new state in the Command response, using optimistic updates in the UI, or waiting for confirmation via polling.
3. Applying CQRS where a CRUD would do
The cost of introducing CommandHandlers, QueryHandlers, two models and a synchronization strategy in an application that only creates, reads, updates and deletes records without complex domain rules is a real cost with no measurable return. CQRS isn't a code organization pattern — it's a pattern for solving specific scalability and modeling problems. Apply it where the problem exists, not as a project-wide convention.
4. Mixing write logic into the QueryHandler
The separation only has value if maintained with discipline. QueryHandlers that update view counters, log access, or change any state collapse the separation and make queries unpredictable and non-idempotent. If a read operation needs to produce a side effect, that effect should be modeled as a separate Command, triggered after the Query or in parallel.
5. Fragile Read Model synchronization
A Read Model updated synchronously inside the write transaction cancels the benefits of decoupling. One updated via an asynchronous event without delivery guarantees, without idempotency in the handler, and without lag monitoring creates silent inconsistencies that are hard to debug. Read Model synchronization is a critical part of the architecture and needs attention equal to that given to the models themselves.
Related architectures and patterns
Event-Driven Architecture is the most common mechanism for syncing the Read Model in CQRS's full degree. When the CommandHandler persists a Command, it also publishes a domain event; a consumer on the read side receives that event and updates the Read Model. The two patterns complement each other, but are independent: EDA doesn't require CQRS, and simple-degree CQRS doesn't require EDA.
Clean Architecture and CQRS fit together naturally: CommandHandlers and QueryHandlers map to Use Case Interactors; the Write Model lives in the domain layer; the Read Model lives in the adapters or infrastructure layer. The dependency rule is preserved — the handlers define repository interfaces that the infrastructure implements.
In Microservices, CQRS with separate databases is especially valuable because each service can maintain its own Read Model designed for its own needs, avoiding cross-service joins. Synchronization happens via events published on the inter-service bus.