Design Patterns CQRS
Command Query Responsibility Segregation (CQRS) splits an application into two sides: one that changes data and one that reads data. Each side gets its own model, tuned for its job. Systems with heavy reading or complex writing gain clarity and speed.
The Real-World Analogy
A library separates the returns desk from the search catalog. The returns desk updates records when books come back, and it follows strict rules. Searches get quick answers from the catalog, which never alters anything. Each area serves a different purpose, and each runs best without the burden of the other.
Problem With One Model
┌───────────────────┐
writes ──▶ │ │ ◀── reads
(strict │ One Order │ (many screens,
rules) │ model │ joins, reports)
└───────────────────┘
The shared model grows complicated. Reports slow down order processing.
Solution With CQRS
┌──────────────┐ changes ┌─────────────┐
Command ────────▶ │ Command Side │ ──────────▶ │ Write Store │
(change data) │ (rules) │ └──────┬──────┘
└──────────────┘ │ sync
▼
┌──────────────┐ reads ┌─────────────┐
Query ◀───────── │ Query Side │ ◀────────── │ Read Store │
(get data) │ (fast views) │ └─────────────┘
└──────────────┘
Commands and Queries
Command
Asks the system to do something, such as PlaceOrder or CancelOrder. A command changes state and returns little or nothing.
Query
Asks a question, such as GetOrderSummary. A query returns data and never changes state.
Developers describe the idea with one sentence: asking a question must not change the answer.
Code Example
// Command side
class PlaceOrderCommand {
String customerId;
List items;
}
class OrderCommandHandler {
void handle(PlaceOrderCommand cmd) {
Order order = new Order(cmd.customerId, cmd.items);
order.validate(); // strict business rules
writeRepository.save(order);
eventBus.publish(new OrderPlaced(order.id));
}
}
// Query side
class OrderSummaryQuery {
String orderId;
}
class OrderQueryHandler {
OrderSummary handle(OrderSummaryQuery q) {
return readStore.findSummary(q.orderId); // flat, ready-to-show data
}
}
// Synchronizer
class OrderReadModelUpdater {
void on(OrderPlaced event) {
readStore.insertSummary(event.orderId);
}
}Sync Between the Two Sides
Command ─▶ Write Store ─▶ Event "OrderPlaced" ─▶ Updater ─▶ Read Store
(ready for queries)
Updates reach the read store a short moment after the write. Designers call this eventual consistency. Screens must expect that a new order may take a blink to appear in lists.
Levels of CQRS
| Level | Description |
|---|---|
| Light | Separate command and query classes, one database |
| Medium | Separate models and optimized read tables |
| Full | Separate databases, event-driven sync |
Most teams start with the light level and climb only when needs demand it.
Real Projects Using CQRS
- Online stores that serve millions of product views and a few thousand orders.
- Booking platforms with complex rules for changes and simple read pages.
- Reporting dashboards that need pre-calculated tables.
When to Avoid CQRS
Simple create-read-update-delete applications gain little. Extra layers, duplicate models, and synchronization code add cost without matching benefit. Reserve the pattern for domains with real differences between reading and writing needs.
Advantages and Disadvantages
| Advantages | Disadvantages |
|---|---|
| Each side scales on its own | More moving parts |
| Read models fit screens exactly | Eventual consistency surprises users |
| Write rules stay clean | Debugging spans two stores |
Step-by-Step Walkthrough
Trace one order through the code example. A customer submits PlaceOrderCommand. The handler builds an Order, validates it, and saves it to the write store. Publishing an OrderPlaced event follows. An updater receives the event and inserts a summary row into the read store.
A later request, OrderSummaryQuery, asks the query handler for that row. The handler returns flat data ready for the screen, with no joins and no rule checks.
Handling Delays
Because the read store updates slightly after the write, a customer may reload a page and see old data for a moment. Teams reduce confusion in three ways: show a "processing" label, return the new data straight from the command response, or refresh the view after the event arrives.
Common Mistakes
- Letting queries change state, which breaks the core rule.
- Applying CQRS to a simple form application.
- Skipping monitoring of the sync step, which hides stuck updates.
Key Takeaways
- CQRS separates changing data from reading data.
- Commands alter state, and queries only report it.
- Events keep the read side current.
- Adopt the pattern gradually and only for demanding domains.
