The Problem: One Model Trying to Do Everything
Imagine an Order Service.
It needs to support operations such as:
Create Order
Update Order
Cancel Order
Get Order
Search Orders
Get Order History
Get Customer Orders
Get Order Dashboard
Get Sales Report
At first, this is simple.
You might have:
Order Service
|
v
Order Model
|
v
Database
The same model is used for:
Writes
|
+-- Create
+-- Update
+-- Delete
Reads
|
+-- Get by ID
+-- Search
+-- Reports
+-- Dashboards
This is perfectly reasonable.
For many applications, it is exactly what you should build.
But as the system becomes more complicated, something interesting happens.
The requirements for reading data and writing data start becoming very different.
The write side may need:
Business rules
Validation
Transactions
Concurrency control
Domain logic
Aggregates
Authorization
Consistency
while the read side may need:
Fast queries
Denormalized data
Joins
Filtering
Sorting
Pagination
Aggregations
Search
Caching
Read replicas
Now one model is trying to satisfy two very different responsibilities.
This is where CQRS becomes useful.
What Is CQRS?
CQRS stands for:
Command Query Responsibility Segregation
The fundamental idea is:
Separate operations that change state from operations that read state.
Instead of treating the application as one CRUD model:
Application
|
Order Model
/ \
Write Read
\ /
\ /
Database
we separate the responsibilities:
Application
|
+-----------+-----------+
| |
v v
Command Side Query Side
| |
v v
Write Model Read Model
| |
v v
Database Read Store
The important thing is not necessarily that there are two databases.
The important thing is:
Command responsibility
≠
Query responsibility
Martin Fowler describes CQRS as using a different model for updating information than the model used for reading it. He also emphasizes that CQRS is useful in particular situations but adds complexity and is not appropriate for most ordinary systems.
That distinction is extremely important.
CQRS Is Not "Two Databases"
This is probably the most common misunderstanding about CQRS.
People often hear:
CQRS
and immediately think:
Write Database
|
v
Kafka
|
v
Read Database
That is one possible architecture.
It is not CQRS itself.
CQRS can exist with:
One application
One database
Two models
For example:
Application
|
+----------+----------+
| |
v v
Command Model Query Model
| |
+----------+----------+
|
PostgreSQL
The command and query models can use the same underlying database.
Microsoft's architecture guidance explicitly describes this as a basic form of CQRS: separate write and read models while using a shared data store.
You can then evolve toward:
Command Model
|
v
Write Database
Query Model
|
v
Read Database
if the system actually benefits from that separation.
Start With CRUD
To understand why CQRS exists, let's first understand the traditional CRUD approach.
Suppose we have:
orders
with:
id
customer_id
status
total_amount
shipping_address
billing_address
created_at
updated_at
A traditional application might have:
class Order {
Long id;
Long customerId;
Status status;
BigDecimal totalAmount;
Address shippingAddress;
Address billingAddress;
Instant createdAt;
Instant updatedAt;
}
The same object might be used for:
Create
Read
Update
Delete
This is simple.
For example:
GET /orders/123
might return:
{
"id": 123,
"customerId": 456,
"status": "SHIPPED",
"totalAmount": 120.00,
"shippingAddress": {
"city": "Kathmandu"
}
}
And:
PUT /orders/123
might receive:
{
"status": "CANCELLED"
}
The same underlying representation is involved in both operations.
For a simple application, this is excellent.
The problem begins when the application becomes much more complicated.
Reads and Writes Have Different Goals
Consider what happens when we create an order.
The system might need to enforce:
Order must contain at least one item
Customer must be active
Inventory must be available
Payment must be authorized
Order cannot be cancelled after shipment
Total must be calculated correctly
The write side therefore needs substantial business logic.
But consider the order dashboard.
It might need:
Order ID
Customer Name
Product Names
Payment Status
Shipment Status
Warehouse
Total
Discount
Last Updated
The dashboard may need information from:
orders
customers
payments
shipments
products
warehouses
A query could look like:
SELECT
o.id,
c.name,
p.status,
s.status,
SUM(oi.quantity * oi.price) AS total
FROM orders o
JOIN customers c ON ...
JOIN payments p ON ...
JOIN shipments s ON ...
JOIN order_items oi ON ...
GROUP BY ...
Notice what happened.
The write model is about:
Maintaining correct business state
The read model is about:
Efficiently answering a particular question
Those are different problems.
The Fundamental Separation
CQRS gives us two concepts.
Commands
A command represents an intention to change state.
Examples:
CreateOrder
CancelOrder
PayOrder
ShipOrder
AddItemToOrder
RemoveItemFromOrder
ChangeShippingAddress
A command answers:
What does the user want the system to do?
Commands should generally represent business actions rather than low-level field mutations.
For example:
BookHotelRoom
is more meaningful than:
SetReservationStatus = RESERVED
Microsoft's CQRS guidance explicitly recommends task-oriented commands because they better express business intent.
Queries
A query asks for information.
Examples:
GetOrder
SearchOrders
GetCustomerOrders
GetOrderDashboard
GetOrderHistory
GetSalesSummary
A query answers:
What information does the caller need?
A query should not change application state.
This is closely related to the older Command Query Separation (CQS) principle: commands change state, while queries return information without changing observable state.
CQRS takes that idea beyond individual methods and applies it to application architecture.
CQRS at the API Level
A traditional CRUD API might look like:
POST /orders
GET /orders/{id}
PUT /orders/{id}
DELETE /orders/{id}
The CQRS distinction is more conceptual:
Command API
|
+-- CreateOrder
+-- CancelOrder
+-- ShipOrder
+-- ChangeAddress
Query API
|
+-- GetOrder
+-- SearchOrders
+-- GetOrderHistory
+-- GetOrderDashboard
You do not necessarily need separate HTTP services.
You can implement both inside the same application.
The important distinction is:
Commands
↓
Business behavior
Queries
↓
Data retrieval
A Simple CQRS Architecture
The simplest useful implementation could be:
Client
|
+-----+-----+
| |
v v
Commands Queries
| |
v v
Command Handlers Query Handlers
| |
v v
Write Model Read Model
| |
+-----+-----+
|
PostgreSQL
There are still:
One application
One database
but there are two separate models.
This is often the best place to start.
Why Separate the Models?
Because the models have different jobs.
The write model might look like:
Order
|
+-- Customer
+-- OrderItem
+-- Payment
+-- Shipment
It may be highly normalized and designed around domain invariants.
The read model might look like:
OrderSummary
+---------+----------+---------+---------+
| orderId | customer | status | total |
+---------+----------+---------+---------+
It is designed around what users need to see.
The two models don't have to look alike.
That is one of the most important ideas in CQRS.
The Write Model
The write model is responsible for changing state correctly.
It typically contains:
Commands
Command Handlers
Domain Model
Validation
Business Rules
Transactions
Repositories
Concurrency Control
For example:
CreateOrderCommand
|
v
CreateOrderHandler
|
v
Order Aggregate
|
v
OrderRepository
|
v
Database
The write side asks:
Is this operation allowed?
not simply:
Can I update this row?
The Query Model
The query model has a completely different goal.
It asks:
What is the fastest and simplest way to answer this query?
For example:
GetOrderDashboard
might return:
{
"orderId": 123,
"customer": "Alice",
"status": "SHIPPED",
"payment": "PAID",
"shipment": "IN_TRANSIT",
"total": 120.00
}
The query model does not need to reconstruct the entire domain object.
It can simply return a DTO designed for that screen.
Microsoft's guidance describes queries as returning DTOs optimized for presentation and without domain logic.
The Read Model Can Be Denormalized
Suppose the write database has:
customers
orders
order_items
products
payments
shipments
The read model could instead have:
order_dashboard
containing:
order_id
customer_name
order_status
payment_status
shipment_status
total_amount
item_count
Now the dashboard query is trivial:
SELECT *
FROM order_dashboard
WHERE order_id = 123;
Instead of repeatedly performing:
JOIN
JOIN
JOIN
JOIN
GROUP BY
for every request.
This is one of the major benefits of CQRS.
Materialized Views
A read model is often implemented as a materialized view or a projection.
Conceptually:
Write Model
|
| Events / changes
v
Projection
|
v
Read Model
For example:
OrderCreated
OrderPaid
OrderShipped
can update:
order_dashboard
so that the read side always has a precomputed representation.
Instead of calculating:
Customer + Order + Payment + Shipment
for every request, we calculate it when the underlying data changes.
Then:
GET /orders/123
becomes very cheap.
CQRS With One Database
Let's start with the least complicated architecture.
Application
|
+----------+----------+
| |
v v
Command Handler Query Handler
| |
v v
Write Model Read Model
| |
+----------+----------+
|
Database
The write side might use:
JPA/Hibernate
while the query side might use:
JdbcTemplate
jOOQ
native SQL
There is no reason the read side has to use the same ORM model.
For example:
public interface OrderQueryRepository {
OrderSummary findById(Long orderId);
List<OrderSummary> search(
String customer,
String status
);
}
The implementation could use a highly optimized SQL query.
Meanwhile:
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
}
is responsible for the domain model.
That is already CQRS.
Why Use Different Data Access Technologies?
The write side may benefit from:
ORM
Entity Mapping
Aggregate Mapping
Transactions
Optimistic Locking
The read side may benefit from:
SQL
Projections
Database Views
Indexes
Denormalized Tables
Search Engines
Caching
Trying to force both sides through the same abstraction can create unnecessary complexity.
CQRS gives each side freedom to use the right tool.
Separate Databases
Now suppose the application grows.
Reads are:
100,000 requests/sec
Writes are:
1,000 requests/sec
The requirements are obviously different.
We can scale the read side independently:
Client
|
+---------+---------+
| |
v v
Commands Queries
| |
v v
Write Service Query Service
| |
v v
Write Database Read Database
|
+------+------+
| | |
DB Cache Search
Now CQRS becomes a scaling architecture.
AWS describes this as a useful scenario when command and query workloads have different throughput, latency, or consistency requirements.
Read Replicas vs CQRS
This distinction matters.
Suppose we have:
Primary Database
|
+---- Read Replica 1
+---- Read Replica 2
+---- Read Replica 3
This improves read scalability.
But it does not necessarily mean we have CQRS.
Why?
Because we might still have:
Same Model
Same Queries
Same Representation
with the only difference being:
Database replication
CQRS is about:
Responsibility
+
Model separation
Read replicas are primarily about:
Scaling reads
They can be used together, but they are not the same thing.
CQRS With Different Storage Technologies
Once reads and writes are separated, the read side does not necessarily need to use the same database technology.
For example:
Write Side
|
v
PostgreSQL
while:
Read Side
|
v
Elasticsearch
or:
Write Side
|
v
PostgreSQL
Read Side
|
v
Redis
or:
Write Side
|
v
PostgreSQL
Read Side
|
v
MongoDB
The choice should be driven by query requirements.
For example:
Relational database
|
+-- Strong transactions
+-- Complex writes
+-- Referential integrity
Search engine
|
+-- Full-text search
+-- Flexible filtering
+-- Search ranking
CQRS allows the read side to use the storage technology that best fits its workload.
How Does the Read Model Get Updated?
This is where things become more interesting.
Suppose:
Order Service
updates:
orders
but the read model lives in:
order_read_model
Something needs to synchronize them.
A common architecture is:
Command
|
v
Write Model
|
v
Write Database
|
v
Event
|
v
Read Model Projector
|
v
Read Database
For example:
CreateOrder
|
v
Order Service
|
+---- PostgreSQL
|
+---- OrderCreated
|
v
Read Model Projector
|
v
order_dashboard
Now:
Write database
and:
Read database
are separate.
This Introduces Eventual Consistency
Suppose the user creates an order.
At:
10:00:00.000
the write succeeds.
But the read projection receives the event at:
10:00:00.150
For 150 milliseconds:
Write Model:
Order exists ✓
Read Model:
Order does not exist yet
This is eventual consistency.
The read model eventually catches up.
Microsoft explicitly identifies eventual consistency as one of the main trade-offs of separating read and write data stores.
Eventual Consistency Is Not Automatically a Bug
It depends on the requirement.
For:
Analytics dashboard
a delay of:
1 second
may be completely acceptable.
For:
Bank account balance
it may not be acceptable to show stale data after a transaction.
Therefore, CQRS should be driven by business requirements.
Ask:
How stale can the read model be?
before introducing asynchronous projections.
The Read-After-Write Problem
A common problem appears immediately.
Suppose:
POST /orders
creates:
Order 123
The client immediately calls:
GET /orders/123
But the query model has not processed the event yet.
The response might be:
404 Not Found
even though the order was successfully created.
This is a classic CQRS problem.
Possible solutions include:
Return the write-side result
or:
Read from the write model immediately after a write
or:
Use consistency tokens
or:
Wait until the projection reaches a required version
or:
Use synchronous projection for critical paths
There is no universal solution.
The right choice depends on the consistency requirements of the application.
Strong Consistency Where It Matters
CQRS does not mean:
Everything must be eventually consistent.
You can have:
Command
|
v
Write Model
|
+---- Critical read → Write Model
|
+---- Dashboard → Read Model
For example:
Account balance
may be read from the authoritative write model.
While:
Monthly spending dashboard
may use an eventually consistent projection.
This is often a much more practical architecture than forcing every read through an eventually consistent read store.
CQRS Does Not Require Messaging
Another common misconception is:
CQRS = Kafka
No.
You can have:
Command Model
|
v
Database
|
v
Query Model
without a message broker.
You can even have:
One database
One application
Two models
and still have CQRS.
Messaging becomes useful when:
Read model is separate
and needs asynchronous updates.
But messaging is an implementation choice, not the definition of CQRS.
CQRS Does Not Require Event Sourcing
This distinction is extremely important.
You can have:
CQRS
+
Traditional relational database
without Event Sourcing.
For example:
Command
|
v
orders table
and:
Query
|
v
order_dashboard table
That is CQRS.
Event Sourcing is a different pattern.
With Event Sourcing:
Events
|
+-- OrderCreated
+-- OrderPaid
+-- OrderShipped
|
v
Current State
The events are the source of truth.
With ordinary CQRS:
Current State
|
+-- Write Model
|
+-- Read Model
The write database can remain the source of truth.
Microsoft also treats CQRS and Event Sourcing as separate patterns that can be combined but do not have to be.
CQRS + Event Sourcing
When the two patterns are combined:
Command
|
v
Command Handler
|
v
Event Store
|
+---------+---------+
| |
v v
Event Stream Projection Engine
|
v
Read Database
For example:
OrderCreated
OrderPaid
OrderShipped
are stored permanently.
The read model is built from them:
Events
|
v
Projection
|
v
Order Dashboard
One major advantage is that if you need a new read model, you can rebuild it from the event history.
But this also introduces significantly more complexity.
CQRS + Transactional Outbox
This is where the previous article in this series becomes directly relevant.
Suppose the write model performs:
Create Order
and needs to publish:
OrderCreated
We have the same dual-write problem:
Write Database
+
Message Broker
The Transactional Outbox solves it.
The write side does:
BEGIN
Create Order
Insert OrderCreated into Outbox
COMMIT
Then:
Outbox
|
v
Publisher
|
v
Message Broker
|
v
Read Model Projector
|
v
Read Database
So now the patterns fit together naturally:
CQRS
|
+-- Separate command and query responsibilities
|
+-- Outbox reliably publishes changes
|
+-- Broker distributes events
|
+-- Projection builds read models
This is a very common architecture for event-driven systems.
CQRS and the Outbox Are Different
It is useful to keep their responsibilities separate.
CQRS
|
+-- Separates reads from writes
while:
Transactional Outbox
|
+-- Makes database change + outgoing event atomic
And:
Event Broker
|
+-- Transports events
And:
Projection
|
+-- Builds read model
So:
CQRS ≠ Outbox
CQRS ≠ Kafka
CQRS ≠ Event Sourcing
CQRS ≠ Microservices
They can work together.
But they solve different problems.
A Complete CQRS Architecture
Let's put everything together.
Client
|
+-------------+-------------+
| |
v v
Commands Queries
| |
v v
Command Handler Query Handler
| |
v v
Write Model Read Model
| |
v v
Write Database Read Database
|
v
Outbox
|
v
Message Broker
|
v
Read Projector
|
v
Read Database
The responsibilities are now clearly separated.
Command Side
|
+-- Business rules
+-- Validation
+-- Transactions
+-- Consistency
+-- State changes
Query Side
|
+-- Fast retrieval
+-- DTOs
+-- Filtering
+-- Sorting
+-- Search
+-- Read optimization
A Real Example: Order Management
Suppose we are building an e-commerce platform.
The business has:
10 million orders
Users frequently perform:
Search orders
View order details
Filter by status
View dashboard
View customer history
But writes are relatively infrequent.
For example:
Reads:
1,000,000 / minute
Writes:
20,000 / minute
The traditional model might struggle because the same database has to support:
Complex writes
+
Massive read traffic
CQRS allows us to separate them.
Command Side
The command:
CreateOrder
goes to:
Command Handler
The handler:
Validate request
|
v
Check business rules
|
v
Create Order
|
v
Persist Order
|
v
Write Outbox
The transaction is:
BEGIN
INSERT order
INSERT outbox event
COMMIT
Query Side
The event:
OrderCreated
is published.
The projector receives it:
OrderCreated
|
v
Read Model Projector
|
v
INSERT INTO order_dashboard
Now the query:
GET /orders/123
can simply execute:
SELECT *
FROM order_dashboard
WHERE order_id = 123;
The read model is optimized specifically for that query.
Different Read Models for Different Screens
This is one of the most powerful aspects of CQRS.
You don't need one universal read model.
You can have:
Order Details Projection
for:
GET /orders/{id}
and:
Customer Orders Projection
for:
GET /customers/{id}/orders
and:
Order Dashboard Projection
for:
GET /dashboard/orders
and:
Sales Report Projection
for:
GET /reports/sales
Conceptually:
Events
|
+------------+------------+
| | |
v v v
Order View Customer View Sales View
Each projection is optimized for a particular query workload.
This is sometimes called polyglot read modeling or simply maintaining multiple read projections.
Why This Can Be Extremely Fast
Suppose a dashboard requires:
20 joins
and:
5 aggregations
If every request executes those operations:
Request
|
v
20 joins
|
v
5 aggregations
|
v
Response
you are repeating expensive computation.
With a projection:
Write Event
|
v
Compute once
|
v
Store result
Then:
Request
|
v
SELECT precomputed row
|
v
Response
The expensive work happens when data changes rather than every time data is read.
CQRS as Precomputation
This gives us another useful mental model.
Traditional querying:
Every request
|
v
Compute answer
CQRS projection:
Data changes
|
v
Compute answer
|
v
Store answer
|
v
Every request reads answer
So CQRS can effectively turn expensive query computation into an asynchronous preprocessing step.
Scaling the Read Side
Suppose:
Command traffic:
1,000 requests/sec
Query traffic:
100,000 requests/sec
With CQRS:
Queries
|
+---------+---------+
| | |
v v v
Query-1 Query-2 Query-3
| | |
+---------+---------+
|
Read Database
You can scale the query side independently.
Meanwhile:
Commands
|
v
Command Service
|
v
Write Database
This avoids scaling the entire application based on the dominant read workload.
Microsoft identifies independent scaling as one of CQRS's major benefits.
Scaling the Write Side
The write side has different constraints.
Writes often involve:
Transactions
Locks
Concurrency
Business invariants
Aggregate consistency
You may not want to blindly add hundreds of write instances.
Instead, you might keep:
Smaller command-processing cluster
while having:
Large query cluster
because the workloads are fundamentally different.
CQRS and Concurrency
CQRS can also help with concurrency problems.
Suppose two users try:
Buy last available ticket
at the same time.
The command side can enforce:
Only one command succeeds.
The write model can use:
Optimistic locking
Pessimistic locking
Database constraints
Aggregate versioning
The read model does not need to enforce these business invariants.
Its job is:
Show the current projection.
This keeps the complex consistency logic concentrated on the write side.
Optimistic Concurrency
For example:
Order version = 10
Client sends:
CancelOrder
expectedVersion = 10
The command handler executes:
UPDATE orders
SET status = 'CANCELLED',
version = 11
WHERE id = 123
AND version = 10;
If zero rows are updated:
Someone else changed the order.
The command fails.
This is a write-side concern.
The query side simply reads:
Order status
It does not need to understand the concurrency protocol.
Commands Should Express Intent
Consider:
UpdateOrderStatus
versus:
ShipOrder
The second is often a better command.
Why?
Because:
ShipOrder
can express business rules:
Payment must be completed
Inventory must be reserved
Order must not already be cancelled
Shipment address must exist
while:
UpdateOrderStatus
can allow callers to bypass those rules.
A command should therefore represent:
Business intent
rather than:
Database mutation
This is one of the most important modeling ideas in CQRS.
Commands Are Not CRUD Requests
A weak CQRS implementation might simply rename CRUD:
CreateOrderCommand
UpdateOrderCommand
DeleteOrderCommand
and call it CQRS.
That misses much of the value.
A richer domain might use:
PlaceOrder
AddItemToOrder
RemoveItemFromOrder
ApplyDiscount
ConfirmPayment
ShipOrder
CancelOrder
RefundOrder
Each command represents an action.
The command handler becomes the place where the business operation is implemented.
Queries Should Be Boring
This is a useful CQRS principle.
The command side should contain:
Complexity
The query side should ideally contain:
Simple retrieval
For example:
public OrderDetails getOrder(long orderId) {
return jdbcTemplate.queryForObject(
"""
SELECT ...
FROM order_details_view
WHERE order_id = ?
""",
mapper,
orderId
);
}
There should be little or no:
Business decision
Mutation
Transaction
Domain behavior
in the query handler.
The query side should answer:
"Here is the data you asked for."
Security Benefits
CQRS can also help with security.
The command side can expose only:
Allowed business operations
For example:
CancelOrder
RefundOrder
ChangeAddress
instead of allowing clients to submit arbitrary:
UPDATE orders
SET ...
The query side can expose only the fields appropriate for a particular user.
For example:
Customer View
might contain:
Order ID
Status
Total
Shipment
while:
Internal Finance View
might contain:
Payment Provider
Transaction ID
Fees
Tax
Settlement Status
Separate read models can therefore support different security boundaries.
Microsoft identifies separation of read and write responsibilities as a potential security benefit of CQRS.
CQRS and Caching
CQRS works particularly well with caching.
For example:
Read Model
|
v
Redis
The query side can serve:
GET /product/123
directly from a cache.
The write side remains authoritative:
PostgreSQL
When the product changes:
ProductUpdated
|
v
Invalidate / update cache
The key is that the cache is part of the read strategy, not the source of truth.
CQRS and Search
Search is another strong use case.
Suppose your transactional database is:
PostgreSQL
but users need:
Full-text search
Fuzzy matching
Filtering
Ranking
Facets
A query model could be:
PostgreSQL
|
v
ProductUpdated
|
v
Search Projection
|
v
Elasticsearch
Now:
Commands
|
v
PostgreSQL
while:
Queries
|
v
Elasticsearch
The write model remains focused on transactional correctness.
The query model is optimized for search.
CQRS and Reporting
Reporting is another common reason to separate reads.
Transactional schemas are often optimized for:
OLTP
while reports need:
Aggregations
Joins
Historical analysis
Grouping
Large scans
Instead of making the transactional database perform huge analytical queries:
Production DB
|
+-- Transactions
+-- User requests
+-- Reports
you can create a dedicated projection:
Production DB
|
v
Events / ETL
|
v
Reporting Store
|
v
Reports
Again, the read model is optimized for its workload.
CQRS and Microservices
CQRS does not require microservices.
You can implement CQRS inside a monolith:
Monolith
|
+-- Command Module
|
+-- Query Module
|
+-- Database
This can actually be a very good starting point.
Later, if the system needs independent deployment or scaling:
Command Module
|
v
Command Service
Query Module
|
v
Query Service
The architecture can evolve without changing the fundamental responsibility separation.
CQRS Inside a Monolith
A practical Java project might look like:
src/main/java/com/example/order
command/
CreateOrderCommand.java
CreateOrderHandler.java
CancelOrderCommand.java
CancelOrderHandler.java
query/
GetOrderQuery.java
GetOrderHandler.java
SearchOrdersQuery.java
SearchOrdersHandler.java
domain/
Order.java
OrderItem.java
OrderRepository.java
infrastructure/
OutboxRepository.java
This gives you CQRS without introducing:
Kafka
Multiple databases
Microservices
Distributed transactions
That is often the right way to learn and adopt CQRS.
A Practical Spring Boot Structure
For example:
REST API
|
+------------+------------+
| |
v v
Command Controller Query Controller
| |
v v
Command Handler Query Handler
| |
v v
Domain Model Read Repository
| |
v v
JPA Repository JDBC / jOOQ
| |
+------------+------------+
|
PostgreSQL
The command side can use:
Spring Data JPA
while the query side can use:
JdbcTemplate
or:
jOOQ
There is no requirement that both use the same persistence model.
The Query DTO Is Not the Domain Entity
This is another important distinction.
Avoid:
OrderEntity
being returned directly from every query.
Instead:
public record OrderSummary(
Long orderId,
String customerName,
String status,
BigDecimal total
) {}
The query model can return exactly what the client needs.
For example:
OrderSummary
OrderDetails
OrderDashboard
CustomerOrderHistory
SalesSummary
Each can have a different shape.
That is one of the biggest practical benefits of separating read and write models.
Multiple Read Models
Suppose we have:
OrderCreated
OrderPaid
OrderShipped
OrderCancelled
We can build:
Events
|
+-----------+-----------+
| | |
v v v
Order Details Dashboard Reporting
Projection Projection Projection
| | |
v v v
Database Redis Analytics DB
Each read model has a different purpose.
This is difficult to achieve cleanly when every query is forced through the same domain model.
Rebuilding a Read Model
One of the great benefits of event-driven CQRS is the ability to rebuild projections.
Suppose:
Order Dashboard Projection
has a bug.
If the authoritative history is available:
Events
you can rebuild:
DROP old projection
Replay events
OrderCreated
OrderPaid
OrderShipped
...
and produce:
New Projection
This becomes particularly powerful when CQRS is combined with Event Sourcing.
But if your CQRS implementation uses only current-state tables and no durable event history, rebuilding a projection may require:
Database queries
ETL
Backfills
Reprocessing
So projection rebuildability depends on the underlying architecture.
Projection Failures
Suppose:
OrderCreated
is successfully published.
But:
Read Model Projector
is down.
Then:
Write Database:
Order exists ✓
Read Database:
Order missing
This is not necessarily data loss.
The event can remain in:
Broker
and the projector can catch up later.
This is one of the benefits of asynchronous read models.
A temporary read-side failure does not necessarily stop the write side.
Projection Lag
Just as the Outbox has:
Outbox Lag
a CQRS system has:
Projection Lag
For example:
Event timestamp:
10:00:00
Projection processed:
10:00:02
Projection lag:
2 seconds
This should be monitored.
Useful metrics include:
Projection lag
Events processed/sec
Projection failures
Retry count
Consumer lag
Dead-letter messages
Read model freshness
A CQRS architecture without observability can be extremely difficult to debug.
Handling Duplicate Events
If the read model is updated from a message broker, duplicate events are possible.
For example:
OrderCreated
OrderCreated
The projection should be idempotent.
One approach is to track:
event_id
and reject events that have already been processed.
For example:
processed_events
+----------------+
| event_id |
+----------------+
| abc-123 |
+----------------+
The projector can perform:
BEGIN
Check event ID
If already processed:
ignore
Otherwise:
update projection
record event ID
COMMIT
This is another place where the Transactional Outbox + Idempotency article becomes directly relevant.
CQRS and Eventual Consistency Require UX Design
Eventually consistent systems can produce confusing user interfaces.
Imagine:
User:
Create Order
Response:
201 Created
Order ID = 123
Then immediately:
GET /orders/123
returns:
404
The system may technically be correct.
But the user experience is terrible.
Therefore CQRS is not only a backend architecture problem.
The UI/API contract must understand:
What becomes available immediately?
What becomes available eventually?
How long can propagation take?
Possible approaches include:
Return command result
or:
Return resource state directly from command side
or:
Expose projection status
For example:
{
"orderId": "123",
"status": "PROCESSING",
"readModelReady": false
}
The architecture must make consistency behavior explicit.
CQRS Does Not Automatically Improve Performance
This is another important misconception.
Simply doing:
Command
+
Query
does not make a system faster.
You gain performance when the separation allows you to do something useful:
Denormalize reads
Scale reads independently
Use specialized databases
Precompute results
Use search engines
Reduce contention
Optimize queries separately
If you have:
Simple CRUD application
and introduce:
Commands
Queries
Handlers
Events
Broker
Read database
Projection
Retries
Monitoring
you may make the system significantly slower to develop and operate.
CQRS is valuable when the separation buys you something meaningful.
CQRS Adds Complexity
A traditional application:
API
|
v
Service
|
v
Database
may become:
API
|
+---- Command Service
| |
| v
| Write DB
|
+---- Query Service
|
v
Read DB
Write DB
|
v
Outbox
|
v
Broker
|
v
Projector
|
v
Read DB
Now you have:
More components
More deployments
More failure modes
More monitoring
More operational work
Eventual consistency
Duplicate handling
Replay concerns
Schema evolution
That complexity must be justified.
Martin Fowler's warning is important here: CQRS can be valuable for a minority of systems, while applying it to systems that do not need it can increase complexity and risk.
When CQRS Is a Good Fit
CQRS is particularly useful when you have:
1. Very Different Read and Write Workloads
For example:
Reads:
1,000,000/sec
Writes:
10,000/sec
Independent scaling can be valuable.
2. Complex Business Rules
For example:
Banking
Insurance
Trading
Order management
Booking systems
where commands represent meaningful domain operations.
The write model can focus heavily on:
Correctness
Consistency
Business invariants
while the query model focuses on:
Presentation
Search
Reporting
3. Complex Queries
If your application constantly needs:
20-table joins
Large aggregations
Search
Multiple projections
a dedicated read model can simplify and accelerate queries.
4. Different Storage Requirements
For example:
Write:
PostgreSQL
Read:
Elasticsearch
or:
Write:
PostgreSQL
Read:
Redis
CQRS makes the architectural separation explicit.
5. Task-Based Applications
Applications where users perform meaningful business actions:
Book Flight
Approve Loan
Cancel Subscription
Reserve Inventory
Process Refund
often benefit from command-oriented modeling.
Microsoft identifies task-based user interfaces and complex domain models as scenarios where CQRS can be beneficial.
When CQRS Is a Bad Fit
Avoid CQRS when your application is mostly:
Create
Read
Update
Delete
with:
Simple business rules
Simple queries
Moderate traffic
One database
No unusual scaling requirements
For example:
Internal admin CRUD application
may not benefit from:
Command handlers
Query handlers
Event bus
Projections
Read databases
A simple:
Controller
|
v
Service
|
v
Repository
|
v
Database
may be much better.
CQRS Is Not a Microservices Pattern
CQRS can exist inside:
Monolith
or:
Modular Monolith
or:
Microservices
or:
Distributed Event-Driven System
Think of CQRS as a modeling and architectural pattern.
Do not automatically equate:
CQRS
=
Microservices
They are independent decisions.
CQRS Is Not an Architecture for Everything
One of the most important lessons is:
CQRS should usually be applied selectively, not blindly across the entire system.
You might have:
Order Management
|
+-- CQRS ✓
User Profile
|
+-- CRUD ✓
Admin Settings
|
+-- CRUD ✓
Reporting
|
+-- Read-optimized model ✓
There is no requirement for every subsystem to use the same architecture.
Microsoft's CQRS journey guidance similarly emphasizes that CQRS is not intended to be automatically imposed as the top-level architecture for an entire system.
A Good Evolution Path
Do not start with:
Microservices
Kafka
CQRS
Event Sourcing
Multiple Databases
Kubernetes
just because the system might eventually need them.
Start simple.
Stage 1 - CRUD
API
|
v
Service
|
v
Database
Stage 2 - Separate Code Responsibilities
Commands
Queries
but:
Same Database
Stage 3 - Optimize Read Model
Introduce:
Read-specific DTOs
Native SQL
Denormalized tables
Indexes
Stage 4 - Separate Read Storage
Only if required:
Write DB
|
v
Read DB
Stage 5 - Introduce Events
Write DB
|
v
Outbox
|
v
Broker
|
v
Projection
|
v
Read DB
Stage 6 - Consider Event Sourcing
Only if the domain actually benefits from:
Event history
Replay
Auditability
Temporal reconstruction
Multiple projections
This incremental approach keeps complexity proportional to the actual problem.
CQRS + Saga + Outbox
Now we can connect the patterns from this series.
Suppose an order requires:
Create Order
Reserve Payment
Reserve Inventory
Arrange Shipment
A Saga coordinates the business workflow:
Saga
|
v
Create Order
|
v
Reserve Payment
|
v
Reserve Inventory
|
v
Arrange Shipment
CQRS separates:
Commands
from:
Queries
Outbox reliably publishes state changes:
Command
|
v
Write Model
|
+---- State
|
+---- Outbox
|
v
Broker
|
v
Other Services
And projections build optimized read models:
Events
|
+---- Order Dashboard
+---- Customer History
+---- Sales Report
So the complete architecture can become:
Client
|
+---------+---------+
| |
v v
Commands Queries
| |
v v
Command Model Query Model
| |
v v
Write DB Read DB
|
Outbox
|
v
Broker
|
+---------+---------+
| | |
v v v
Payment Inventory Shipping
|
v
Events
|
v
Projections
|
v
Read DB
Now each pattern has a clear responsibility.
CQRS
↓
Separates commands and queries
Saga
↓
Coordinates distributed business workflow
Outbox
↓
Reliably publishes local state changes
Broker
↓
Transports events
Projection
↓
Builds optimized read models
Idempotency
↓
Makes duplicate delivery safe
This is much more useful than memorizing the names of the patterns.
A Production Failure Walkthrough
Let's see what happens when things go wrong.
Command Database Fails
CreateOrder
|
v
Write DB
|
X
Failure
The command fails.
No order should be considered committed.
Write Succeeds but Publisher Is Down
Write DB ✓
Outbox ✓
|
X
Publisher down
The read model may not update.
But the event remains durable.
When the publisher returns:
Outbox
|
v
Publisher
|
v
Broker
the event is eventually delivered.
Broker Is Down
Outbox
|
v
Publisher
|
X
Broker unavailable
The publisher retries.
The write side does not necessarily need to be unavailable.
Projector Is Down
Broker
|
X
Projector
The write model remains operational.
The read model becomes stale.
When the projector recovers:
Broker
|
v
Projector
|
v
Read DB
it catches up.
Duplicate Event
OrderCreated
OrderCreated
The projector checks:
eventId
and safely ignores the duplicate.
Read Database Is Down
Queries
|
X
Read DB
The command side may still be able to accept writes.
Whether the application can tolerate this depends on the product requirements.
This is one of the major architectural benefits of separating the two sides:
Read failure
does not necessarily imply:
Write failure
The Deepest Mental Model
Traditional CRUD asks:
"What database object am I manipulating?"
CQRS asks:
"What is the user trying to do, and what information does the user need?"
That changes the architecture.
Instead of:
OrderController
|
v
OrderService
|
v
OrderEntity
you begin thinking in terms of:
Commands
PlaceOrder
CancelOrder
ShipOrder
RefundOrder
and:
Queries
GetOrder
GetCustomerOrders
GetOrderDashboard
GetSalesReport
The write model is designed around:
Business behavior.
The read model is designed around:
Information consumption.
That is the essence of CQRS.
The Three Levels of CQRS
It is useful to think of CQRS as having levels.
Level 1 - Interface Separation
Commands
Queries
with separate handlers.
Same database.
Application
/ \
Commands Queries
| |
v v
Command Model Query Model
\ /
\ /
Database
This is the simplest form.
Level 2 - Model Separation
Now:
Write Model
and:
Read Model
have genuinely different structures.
Write Model
|
v
Normalized Domain Data
Read Model
|
v
Denormalized Query Data
Still potentially one database.
Level 3 - Storage Separation
Now:
Write Model
|
v
Write Database
and:
Read Model
|
v
Read Database
with:
Events
synchronizing them.
This provides the greatest flexibility but also introduces the greatest complexity.
CQRS Is a Spectrum
Therefore, CQRS is not:
ON
or:
OFF
It is better understood as a spectrum.
Simple CRUD
|
v
Separate Commands/Queries
|
v
Separate Models
|
v
Separate Read Projections
|
v
Separate Databases
|
v
Event-driven CQRS
|
v
CQRS + Event Sourcing
You should stop at the level that solves your actual problem.
Final Architecture
A mature CQRS system might look like:
Client
|
+-----------+-----------+
| |
v v
Commands Queries
| |
v v
Command Handler Query Handler
| |
v v
Domain Model Read Model
| |
v v
Write Database Read Database
|
v
Outbox
|
v
Message Broker
|
+---------+---------+
| |
v v
Projection A Projection B
| |
v v
Read Database Search Index
And the consistency model becomes:
Command
|
v
Write Model
|
v
Committed State
|
v
Event
|
v
Projection
|
v
Read Model
There may be a small period where:
Write Model = new state
Read Model = old state
That is the price of asynchronous read modeling.
The architecture is useful when the benefits outweigh that complexity.
Final Takeaway
CQRS is fundamentally about one idea:
The model that changes state does not have to be the same model that reads state.
Traditional CRUD uses:
One model
|
+-- Read
+-- Write
CQRS separates them:
Command Model
|
+-- Business rules
+-- Validation
+-- Transactions
+-- State changes
Query Model
|
+-- DTOs
+-- Read optimization
+-- Search
+-- Reporting
+-- Presentation
The simplest CQRS architecture can still be:
One application
One database
Two models
You can then evolve toward:
Separate read model
|
v
Separate read database
|
v
Event-driven projections
when the system actually needs it.
And this is where the patterns in this series connect:
CQRS
|
| separates
v
Commands and Queries
|
| write changes produce events
v
Transactional Outbox
|
| reliably publishes
v
Message Broker
|
| distributes
v
Read Model Projectors
|
| build
v
Optimized Query Models
If the system is a distributed business workflow:
Saga
|
v
Coordinates the workflow
while:
Outbox
|
v
Reliably publishes each local transition
and:
Idempotency
|
v
Makes duplicate delivery safe
The most important lesson is therefore not:
"Use CQRS because it scales."
It is:
Separate responsibilities when the read side and write side have genuinely different problems to solve.
If your application is simple CRUD, keep it simple.
If your write model needs strong business invariants while your read side needs completely different representations, queries, scaling characteristics, or storage technologies, CQRS can provide a powerful separation.
And if you combine CQRS with:
Transactional Outbox
+
Event-driven projections
+
Idempotent consumers
you get a robust foundation for building highly scalable event-driven systems.
But CQRS itself remains the simple idea underneath all of it:
STATE CHANGE
|
v
COMMAND
|
v
WRITE MODEL
INFORMATION
|
v
QUERY
|
v
READ MODEL
Different questions deserve different models.