CQRS
Mental model
CQRS = Command Query Responsibility Segregation.
Commands change state. Queries read state. They don’t have to use the same model, database, or scaling strategy.
Without CQRS
flowchart LR API --> Service Service --> DB[("One Model / DB")]
The same model handles:
Create Order
Update Order
Get Order
Search Orders
Order HistoryThis is often perfectly fine.
With CQRS
flowchart LR Client --> API["API"] API --> Command["Command Side"] API --> Query["Query Side"] Command --> WriteDB[("Write DB")] WriteDB --> Events["Domain Events"] Events --> ReadDB[("Read Model")] Query --> ReadDB
The write and read sides have different responsibilities.
Command
A command asks the system to perform an action.
CreateOrder
CancelOrder
ReserveInventory
UpdateProductSmall Java record:
public record CreateOrderCommand(
UUID customerId,
List<OrderItem> items
) {}Small .NET record:
public record CreateOrderCommand(
Guid CustomerId,
IReadOnlyList<OrderItem> Items);Query
A query asks for information without changing state.
GetOrder
SearchProducts
GetCustomerOrders
GetInventoryAvailabilityJava:
public record GetOrderQuery(UUID orderId) {}.NET:
public record GetOrderQuery(Guid OrderId);Why Separate Them?
Suppose we have:
Orders written 10,000/sec
Orders read 100,000/secThe read workload is much larger.
With CQRS we can scale independently:
┌── Write Service × 3
Client → API ────┤
└── Read Service × 20The read model can also be shaped specifically for queries.
Read Model
Instead of reconstructing an order from many normalized tables:
orders
order_items
products
customers
shipments
paymentswe can maintain a query-friendly projection:
{
"orderId": "123",
"customerName": "Alice",
"status": "SHIPPED",
"total": 129.99,
"items": [
{"name": "Keyboard", "quantity": 1}
],
"trackingNumber": "ABC123"
}The query doesn’t need to perform a complicated multi-table read every time.
CQRS Does NOT Require Microservices
CQRS can exist inside one application:
One application
│
├── Command handlers
│
├── Query handlers
│
└── One databaseIt can also evolve into separate services/databases.
Interview mistake
CQRS does not automatically mean two microservices, two databases, Kafka, or event sourcing.
Those are possible architectural choices, not the definition of CQRS.
CQRS Does NOT Require Event Sourcing
You can use:
CQRS + PostgreSQLwithout event sourcing.
Or:
CQRS + Kafkawithout event sourcing.
Or:
CQRS + Event Sourcingif the domain actually benefits from it.
Keep these concepts separate.
Simple Spring Structure
class CreateOrderHandler {
OrderId handle(CreateOrderCommand command) {
// validate
// create aggregate
// persist
// publish event
return orderId;
}
}Query side:
class GetOrderHandler {
OrderView handle(GetOrderQuery query) {
return orderReadRepository.find(query.orderId());
}
}Simple .NET Structure
public async Task<Guid> Handle(
CreateOrderCommand command)
{
// validate
// persist
// publish event
return order.Id;
}Query:
public Task<OrderView?> Handle(
GetOrderQuery query)
{
return readRepository.GetAsync(query.OrderId);
}When NOT to Use CQRS
Don’t use it merely because:
“CQRS is a microservices best practice.”
It adds:
- additional models
- synchronization complexity
- eventual consistency concerns
- more code
- more operational components
For a simple CRUD application:
Controller → Service → Repository → DBmay be better.
Remember
CQRS is about separating the responsibility of changing state from reading state.
It is useful when the read/write workloads, models, scaling requirements, or business complexity are sufficiently different.
Interview Questions
Does CQRS improve performance automatically?
No. It gives you architectural freedom to optimize reads and writes independently. The benefit comes from how you exploit that separation.
Does CQRS mean eventual consistency?
Not necessarily. If command and query models share one database, they can be strongly consistent. A separate asynchronously updated read model commonly introduces eventual consistency.
Why have a separate read model?
To optimize the representation and access pattern for queries without forcing the write model to serve every read use case.