What Is CQRS?
CQRS, or Command Query Responsibility Segregation, is an architectural pattern that separates operations that change application state from operations that read application state.
Instead of using the same models, APIs, and data-access paths for both reads and writes, CQRS allows the command side and query side to evolve independently. In simple systems this can mean separate application models over the same database. In more advanced architectures, commands and queries can use completely different data stores optimized for their workloads.
Table of Contents
- Why CQRS Exists
- Commands vs Queries
- How CQRS Works
- CQRS Does Not Require Two Databases
- Separate Read and Write Models
- Synchronizing the Read Model
- Eventual Consistency in CQRS
- CQRS and Event-Driven Architecture
- CQRS and Event Sourcing
- Scaling Reads and Writes Independently
- Failure Scenarios
- When to Use CQRS
- When Not to Use CQRS
- Production Design Example
- Common CQRS Mistakes
- Frequently Asked Questions
- Conclusion
Why CQRS Exists
Many applications use the same model for reading and writing data.
Client
↓
Application
↓
Order Model
↓
Database
The same Order model might support:
- creating an order;
- changing its status;
- cancelling it;
- loading order details;
- searching orders;
- building dashboards;
- generating reports.
This approach is simple and works well for many applications.
Problems appear when read and write workloads have very different requirements.
The write side may need:
- strict validation;
- transactions;
- business invariants;
- normalized relational data;
- concurrency control.
The read side may instead need:
- fast filtering;
- full-text search;
- precomputed aggregates;
- denormalized views;
- high read throughput;
- low-latency responses.
Trying to make one model ideal for both workloads can create unnecessary complexity.
CQRS separates those responsibilities.
Application
/ \
↓ ↓
Commands Queries
↓ ↓
Write Model Read Model
Commands vs Queries
The basic CQRS distinction is between commands, which request state changes, and queries, which retrieve information.
Commands
A command expresses an intention to change state.
Examples include:
CreateOrder
CancelOrder
ChangeShippingAddress
CapturePayment
ReserveInventory
A command usually contains the information required to perform one business operation.
{
"command": "CancelOrder",
"order_id": "order-8472",
"reason": "customer_requested"
}
The command handler loads the relevant state, validates business rules, performs the operation, and commits the result.
CancelOrder
↓
Command Handler
↓
Load Order
↓
Validate Rules
↓
Update State
↓
Commit
A command can fail.
For example:
CancelOrder
↓
Order already shipped
↓
Reject command
Commands therefore represent business behavior rather than generic database updates.
Queries
A query requests information without intentionally changing application state.
Examples include:
GetOrder
SearchOrders
GetCustomerOrderHistory
GetSalesDashboard
GetPendingShipments
The query side does not need to reconstruct the domain model simply to display information.
For example, a dashboard query might directly return:
{
"orders_today": 12482,
"revenue_today": 731204.12,
"pending_shipments": 418,
"failed_payments": 37
}
The read model can be designed specifically around how the application consumes data.
How CQRS Works
A basic CQRS architecture separates command handling from query handling.
Client
/ \
↓ ↓
Command API Query API
↓ ↓
Write Model Read Model
↓ ↓
Write DB Read DB
The write side owns business operations.
For example:
POST /orders
↓
CreateOrder
↓
Command Handler
↓
Domain Rules
↓
Write Database
The read side answers requests using a representation designed for retrieval.
GET /orders/order-8472
↓
Query Handler
↓
Read Model
↓
Response
The two sides can use different application code, schemas, scaling strategies, and storage technologies.
That flexibility is the main advantage of CQRS, but it is also where much of its complexity comes from.
CQRS Does Not Require Two Databases
A common misconception is that CQRS always means maintaining two databases.
It does not.
CQRS can begin as a logical separation inside one application:
Commands ──→ Write Model ─┐
├──→ PostgreSQL
Queries ──→ Read Model ──┘
The command side might use domain entities and repositories while the query side uses optimized SQL directly against the same database.
For example, the command side can enforce business behavior:
def cancel_order(order_id: str):
order = repository.get(order_id)
if order.status == "SHIPPED":
raise OrderCannotBeCancelled()
order.cancel()
repository.save(order)
The query side can simply retrieve the representation required by the UI:
SELECT
o.id,
o.status,
o.total,
c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.id = :order_id;
This already separates command and query responsibilities.
Separate databases are an architectural option, not a requirement.
Separate Read and Write Models
More advanced CQRS architectures maintain physically separate models.
The write model can remain normalized:
orders
order_items
customers
payments
shipments
This structure works well for transactional updates and enforcing relationships.
The read model might instead store a denormalized document:
{
"order_id": "order-8472",
"customer": {
"id": "customer-91",
"name": "Alice"
},
"status": "PAID",
"total": 149.00,
"items": [
{
"name": "Mechanical Keyboard",
"quantity": 1,
"price": 149.00
}
],
"payment_status": "CAPTURED",
"shipment_status": "PENDING"
}
A query can retrieve this object directly without joining several tables at request time.
Different read models can also exist for different use cases:
Write Model
│
├──→ Order Details Read Model
├──→ Customer History Read Model
├──→ Operations Dashboard
└──→ Search Index
The goal is not merely duplicating data. Each read model should exist because it makes an important query workload simpler, faster, or easier to scale.
Synchronizing the Read Model
When read and write stores are physically separate, the system must propagate committed changes from the write side to the read side.
A common approach is event-driven synchronization:
Command
↓
Write Model
↓
Write Database
↓
Domain Event
↓
Message Broker
↓
Projection Worker
↓
Read Model
Suppose an order is created.
The write side publishes:
{
"event_id": "evt-551",
"event_type": "OrderCreated",
"order_id": "order-8472",
"customer_id": "customer-91",
"total": 149.00
}
A projection worker consumes the event and updates the query model.
INSERT INTO order_summary (
order_id,
customer_id,
status,
total
)
VALUES (
'order-8472',
'customer-91',
'PENDING',
149.00
);
Later:
PaymentCaptured
↓
Projection Worker
↓
order_summary.status = PAID
The query database gradually follows the authoritative write model.
If reliable publication is required, the write side can use the Transactional Outbox Pattern for Reliable Messaging so a committed state change cannot silently lose the event needed to update projections.
Eventual Consistency in CQRS
When the write model and read model are updated asynchronously, there is normally a delay between them.
Consider:
12:00:00.000
CreateOrder committed
12:00:00.020
OrderCreated published
12:00:00.060
Projection receives event
12:00:00.075
Read model updated
For approximately 75 milliseconds, the order exists in the write model but may not yet appear in the query model.
This creates a read-after-write problem:
Client creates order
↓
Command succeeds
↓
Client immediately queries order
↓
Read model not updated yet
↓
Order appears missing
This is a form of eventual consistency.
Applications need an explicit UX and API strategy for this behavior.
Possible approaches include:
- returning enough state from the command response to update the UI;
- temporarily reading from the authoritative write side;
- waiting for a projection version when strict read-after-write behavior is required;
- showing a processing state until the read model catches up.
Trying to hide eventual consistency without defining its behavior often creates intermittent bugs that are difficult to reproduce.
CQRS and Event-Driven Architecture
CQRS and event-driven architecture work well together, but they are different concepts.
CQRS separates:
Commands
↓
State Changes
Queries
↓
Data Retrieval
Event-driven architecture describes how components communicate about things that happened.
OrderCreated
PaymentCaptured
OrderCancelled
ShipmentCreated
A CQRS application can update read models through events:
Write Model
↓
OrderCreated
↓
Message Broker
↓
Projection
↓
Read Model
This is a common combination because asynchronous events allow multiple projections to be built independently.
The broader communication model is covered in Event-Driven Architecture in Distributed Systems.
CQRS and Event Sourcing
CQRS is frequently discussed together with Event Sourcing, which can make the two patterns appear inseparable.
They solve different problems.
CQRS separates command and query responsibilities.
Command Model
≠
Query Model
Event Sourcing stores changes as a sequence of events instead of storing only the latest state.
OrderCreated
↓
ItemAdded
↓
PaymentCaptured
↓
OrderShipped
The current state can then be reconstructed from that event history.
CQRS can use a normal relational database:
Command
↓
UPDATE orders
SET status = 'PAID'
No Event Sourcing is required.
Similarly, Event Sourcing can technically be used without fully separating every command and query path, although the patterns naturally complement each other.
The important distinction is:
CQRS
→ separates reads and writes
Event Sourcing
→ changes how state is persisted
Scaling Reads and Writes Independently
One major reason to use CQRS is when read and write workloads have very different scale characteristics.
Consider a product platform receiving:
Writes:
5,000 operations/second
Reads:
200,000 queries/second
Using one database model for both workloads can force the transactional database to handle large volumes of read traffic.
CQRS can separate the paths:
Commands
↓
Write API
↓
PostgreSQL
Queries
↓
Query API
↓
Read-Optimized Store
↓
Caches / Replicas
The query side can scale independently without increasing the number of command handlers.
It can also use storage optimized for a specific workload.
For example:
Transactional Writes → PostgreSQL
Product Search → Elasticsearch
Dashboard Queries → Precomputed Read Model
Hot Lookups → Redis
This flexibility can be valuable, but each additional store introduces synchronization, monitoring, deployment, and recovery responsibilities.
CQRS should not be introduced merely because independent scaling is theoretically possible. There should be a real workload or modeling reason for the separation.
Failure Scenarios
A CQRS system with asynchronous projections has more failure modes than a traditional application using one database.
Suppose the write transaction succeeds but the read projection is unavailable:
Command
↓
Write DB ✓
↓
Event ✓
↓
Projection Worker ✗
↓
Read Model stale
The write remains valid. The query side is temporarily behind.
The projection worker should retry processing after recovery.
Another failure can happen after the projection updates its database but before acknowledging the event:
Receive event
↓
Update Read Model ✓
↓
Worker crashes
↓
Acknowledge event ✗
↓
Event delivered again
Projection handlers should therefore usually be idempotent.
For example:
INSERT INTO processed_events (event_id)
VALUES ('evt-551')
ON CONFLICT DO NOTHING;
The system should also be able to detect projection lag.
Useful measurements include:
- oldest unprocessed event age;
- consumer lag;
- projection failure rate;
- event processing latency;
- write-model version vs read-model version;
- dead-lettered projection events.
A healthy command API does not necessarily mean the complete CQRS system is healthy.
When to Use CQRS
CQRS becomes useful when separating reads and writes solves a concrete architectural problem.
Typical signals include:
- complex business rules on the write side;
- read models that differ substantially from transactional models;
- read traffic much larger than write traffic;
- multiple specialized views of the same underlying data;
- expensive joins or aggregations on common query paths;
- independent scaling requirements;
- event-driven workflows that naturally produce projections;
- domain behavior that benefits from explicit commands.
For example, a financial platform may require a strongly controlled transactional model for writes while exposing many read-oriented views for customer dashboards, reporting, support tools, and analytics.
Those workloads do not necessarily need the same data representation.
When Not to Use CQRS
CQRS adds architectural complexity and should not be the default for ordinary CRUD applications.
Consider an internal administration application with:
Users
Products
Categories
Settings
If operations are mostly:
Create
Read
Update
Delete
and one relational schema handles both reads and writes efficiently, splitting the system into commands, events, projections, and multiple data stores may solve no meaningful problem.
A simpler architecture can be:
API
↓
Application
↓
PostgreSQL
Complexity introduced by advanced CQRS can include:
- duplicate data;
- eventual consistency;
- projection workers;
- event schemas;
- retries and idempotency;
- rebuilding projections;
- additional infrastructure;
- more difficult debugging.
The right question is not:
Can CQRS be used here?
It is:
What specific problem becomes easier
because reads and writes are separated?
Production Design Example
Consider an e-commerce platform where order writes require strict business rules, while customers and operations teams generate much larger read traffic.
The write side uses PostgreSQL:
Command API
↓
Order Domain
↓
PostgreSQL
Supported commands include:
CreateOrder
CancelOrder
CapturePayment
MarkOrderShipped
When a customer submits an order:
{
"customer_id": "customer-91",
"items": [
{
"product_id": "product-52",
"quantity": 1
}
]
}
the command handler validates the request and creates the order.
The write transaction also records an outgoing event using a transactional outbox:
BEGIN;
INSERT INTO orders (
id,
customer_id,
status,
total
)
VALUES (
'order-8472',
'customer-91',
'PENDING',
149.00
);
INSERT INTO outbox (
id,
aggregate_id,
event_type,
payload
)
VALUES (
'evt-8472-1',
'order-8472',
'OrderCreated',
'{"order_id":"order-8472","customer_id":"customer-91","total":149.00}'
);
COMMIT;
The event is eventually published to the message broker.
PostgreSQL
↓
Outbox Publisher
↓
Message Broker
↓
Order Projection Worker
The projection worker creates a denormalized customer-facing view:
{
"order_id": "order-8472",
"customer_id": "customer-91",
"status": "PENDING",
"total": 149.00,
"payment_status": "NOT_STARTED",
"shipment_status": "NOT_STARTED"
}
The Query API reads this model directly.
GET /customers/customer-91/orders
↓
Query API
↓
Customer Orders View
Later the Payment service produces:
PaymentCaptured
The projection updates:
payment_status = CAPTURED
status = PAID
A shipping event later updates:
shipment_status = SHIPPED
status = SHIPPED
The write database remains optimized around transactional business operations, while the read model is optimized around customer-facing queries.
Suppose the projection system goes offline for three minutes.
Commands continue succeeding:
Write Side
OrderCreated ✓
PaymentCaptured ✓
OrderShipped ✓
but the read model falls behind:
Read Side
Last processed event:
3 minutes old
Once the projection workers recover, they process the backlog and bring the read model up to date.
Operational monitoring should track:
- command success and rejection rates;
- command processing latency;
- outbox backlog age;
- projection lag;
- projection failures;
- duplicate event handling;
- query latency;
- read-model freshness;
- dead-lettered events.
The most important operational distinction is that write-side availability and read-model freshness are separate health dimensions.
Common CQRS Mistakes
- Assuming CQRS requires two databases. Command/query separation can exist over the same physical database.
- Introducing CQRS for simple CRUD applications. The additional abstractions may provide no practical benefit.
- Confusing CQRS with Event Sourcing. The patterns complement each other but solve different problems.
- Ignoring eventual consistency. Asynchronous read models can temporarily return stale data.
- Publishing events unreliably after write commits. Lost events can leave projections permanently stale.
- Making projection handlers non-idempotent. Duplicate message delivery can corrupt derived data.
- Creating one generic read model. The query side should be designed around actual query requirements.
- Putting business rules into read projections. The write side should remain authoritative for business decisions.
- Assuming the read model is always authoritative. A projection can lag behind the committed write state.
- Ignoring projection recovery. Read models should have a defined strategy for rebuilding or catching up after failures.
- Splitting infrastructure before splitting responsibilities. Separate databases and services should follow a real requirement rather than define CQRS itself.
Frequently Asked Questions
CQRS is often associated with microservices, messaging, and Event Sourcing, which creates several common misconceptions about what the pattern actually requires.
Does CQRS Require Microservices?
No. CQRS can be implemented inside a monolithic application.
Monolith
├── Command Handlers
└── Query Handlers
The architectural principle is the separation of command and query responsibilities, not the number of deployed services.
Does CQRS Require Event Sourcing?
No. A CQRS write model can store normal current state in PostgreSQL, MySQL, or another transactional database.
Event Sourcing is a separate persistence pattern that stores state changes as an event history. CQRS and Event Sourcing can be combined, but neither requires the other.
Does CQRS Require Separate Databases?
No. A simple CQRS architecture can use separate command and query models over the same database.
Separate stores become useful when the read side needs different schemas, technologies, performance characteristics, or independent scaling.
How Does CQRS Handle Read-After-Write?
If the read model is updated asynchronously, an immediate query can temporarily return stale data.
The application can return updated information directly from the command, expose a processing state, temporarily use the authoritative model, or track projection versions when stronger read-after-write behavior is required.
The correct approach depends on how much stale data the business operation can tolerate.
Conclusion
CQRS separates operations that change application state from operations that retrieve information. The command side can focus on business rules, validation, transactions, and consistency, while the query side can focus on efficient data retrieval.
The pattern can be implemented simply with separate command and query models over one database or extended into independent write and read stores synchronized through events.
The additional flexibility comes with costs: duplicated data, eventual consistency, projection recovery, idempotency, messaging reliability, and additional operational complexity.
The core principle is: reads and writes have different responsibilities and sometimes different architectural requirements, so they do not always need to share the same model.
Comments (0)