CQRS (command query responsibility segregation) separates the paths that change state from the paths that read it. A command expresses intent to do something: place an order, capture payment, cancel a shipment. A query asks for a view: order status, invoice history, search results.
The split starts as an interface rule. In larger systems it often becomes two models: a write model optimized for business rules and consistency, and one or more read models optimized for how data is displayed.
Commands vs queries
| Command | Query | |
|---|---|---|
| Purpose | Change state | Return data |
| Side effects | Yes | No |
| Return value | Often an ID or ack, not a full view | The requested view |
| Validation | Business rules and invariants | Access rules and filters |
| Failure mode | Reject the change | Return empty or error |
Queries should not mutate state as a side effect. Commands should not be used as a disguised read when a dedicated query would be clearer.
Simple vs full CQRS
Simple CQRS keeps one database and one domain model, but routes writes and reads through different application entry points. The benefit is clearer code and safer APIs without new infrastructure.
Full CQRS uses separate storage or schemas for writes and reads. The write side commits authoritative state. The read side is rebuilt from events, projections, or replication. Reads may lag behind writes, so the system is often eventually consistent.
flowchart TB Client["Client"] -->|"PlaceOrder command"| WriteAPI["Write API"] Client -->|"GetOrderStatus query"| ReadAPI["Read API"] WriteAPI --> WriteModel["Write model"] WriteModel --> WriteDB["Write store"] WriteModel -->|"domain event"| Broker["Message broker"] Broker --> Projector["Read model projector"] Projector --> ReadDB["Read store"] ReadAPI --> ReadDB
Full CQRS is a scaling and modeling choice, not a default architecture.
Write model and read model
The write model enforces invariants, owns transactions, and records what happened. It is usually normalized, aggregate-oriented, and aligned with domain-driven design inside a bounded context. Event sourcing is one way to implement that write model as an append-only event history.
The read model is shaped for screens, reports, or APIs. It may denormalize data, precompute joins, or index fields the write model does not optimize for. Multiple read models can exist for the same write stream when different consumers need different views.
That duplication is intentional. Integration through events is often cheaper than forcing every query through one shared schema.
How CQRS fits distributed systems
CQRS pairs naturally with asynchronous integration:
- The write side commits local state and publishes facts through the outbox pattern or a broker.
- Projectors, search indexes, caches, and dashboards consume those facts and update read models.
- Consumers use the inbox pattern and idempotency because delivery is usually at-least-once.
This is the same shape as event-driven architecture at scale. Commands become messages with one owner. Queries stay local to the read store that already matches the question.
Relationship to the command pattern
The command pattern encapsulates a request as an object in a single process. CQRS applies the same idea across application boundaries and, in full form, across data stores.
In-process command objects help with undo, logging, and uniform dispatch. Distributed command handlers help with ownership, scaling writes, and keeping read traffic off the transactional path.
When to use it
CQRS helps when:
- Read and write workloads have very different scale or shape.
- The UI needs denormalized views that would complicate the write model.
- Multiple teams or bounded contexts need different projections of the same facts.
- You already operate event-driven architecture and need explicit read-side rebuild paths.
Start with simple CQRS. Promote to separate models only when measured pain justifies the operational cost.
When to skip it
Avoid full CQRS when:
- The application is small and CRUD clarity is enough.
- Every read must reflect the latest write immediately across all views.
- The team cannot own projection lag, replay, and schema drift on the read side.
- The problem is a handful of endpoints, not diverging access patterns.
A single database with clear service methods is often the right answer.
Trade-offs
- Full CQRS adds projectors, monitoring, and failure modes around stale reads.
- Debugging spans write commits, event publication, and read rebuilds.
- Simple CQRS gives most of the clarity benefit with far less infrastructure.
- CQRS complements DDD and EDA, but it does not replace them. Boundaries, language, and delivery guarantees still need explicit design.
