CQRS, or Command Query Responsibility Segregation, is a pattern that separates the operations that change state (Commands) from the operations that read state (Queries).
Imagine you have a system that manages customer orders. A common approach is to have a single OrderService that handles both creating new orders (a Command) and retrieving order details (a Query).
public class OrderService {
// ... database connection and transaction management ...
public Order createOrder(OrderDetails details) {
// Complex logic to validate details, check inventory,
// update stock, process payment, and save to database.
// This operation modifies the order and inventory state.
// ...
return new Order(...);
}
public Order getOrderById(String orderId) {
// Simple query to fetch order details from the database.
// This operation only reads the order state.
// ...
return new Order(...);
}
}
In this monolithic OrderService, the createOrder method might involve complex business logic, inventory checks, and payment processing, all within a single transaction that updates multiple tables. The getOrderById method, on the other hand, is a straightforward database read.
Now, consider the scaling and performance implications. The createOrder operation, being write-heavy and potentially involving complex side effects, might require a robust, transactional database. The getOrderById operation, being read-heavy, could benefit from a highly optimized, denormalized read model that can serve requests quickly. When they share the same service and database, the write operation’s complexity and locking can directly impact the read operation’s performance, and vice-versa.
This is where CQRS comes in. Instead of one service doing both, we split them into two distinct paths:
- Command Side: Handles all state-changing operations. It receives Commands, processes them, and updates the system’s state. This side is optimized for writes.
- Query Side: Handles all state-reading operations. It receives Queries and returns data. This side is optimized for reads.
The key insight is that the data models used for writes and reads can be completely different. The write model is typically a normalized, transactional model that enforces business invariants. The read model is often a denormalized, optimized view tailored for specific query needs.
Let’s look at how this might play out with our OrderService using CQRS.
Command Side:
- Service:
OrderCommandService - Data Model:
OrderWriteModel(e.g., normalizedorders,order_items,inventorytables) - Operations:
createOrder(CreateOrderCommand),cancelOrder(CancelOrderCommand)
public class OrderCommandService {
// ... repository for writing to the normalized datastore ...
public void handle(CreateOrderCommand command) {
// Validate command, check inventory, process payment,
// create OrderWriteModel entities, save to database.
// This is a transactional write.
// ...
}
}
Query Side:
- Service:
OrderQueryService - Data Model:
OrderReadModel(e.g., a denormalizedorders_viewtable optimized for fast lookups byorderId, or even a document database like MongoDB) - Operations:
getOrderById(GetOrderByIdQuery)
public class OrderQueryService {
// ... repository for querying a denormalized read datastore ...
public OrderReadModel handle(GetOrderByIdQuery query) {
// Directly query the optimized read datastore.
// This is a fast, non-transactional read.
// ...
return orderReadModelRepository.findById(query.getOrderId());
}
}
The crucial piece connecting these two sides is how the read model stays up-to-date with changes made by the command side. This is typically achieved through eventual consistency. When a command is successfully processed by the command side, it publishes an event (e.g., OrderCreatedEvent). A separate process, often called an event handler or projection, subscribes to these events and updates the read model accordingly.
@Service
public class OrderEventHandler {
private final OrderReadModelRepository readModelRepository;
public OrderEventHandler(OrderReadModelRepository readModelRepository) {
this.readModelRepository = readModelRepository;
}
@EventListener // Assuming Spring Event listener for simplicity
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
// Transform event data into the denormalized read model format
OrderReadModel readModel = new OrderReadModel();
readModel.setOrderId(event.getOrderId());
readModel.setCustomerName(event.getCustomerName());
readModel.setItems(event.getItems()); // Directly embed items for fast query
readModel.setOrderStatus(event.getOrderStatus());
// ... other fields for the read model
readModelRepository.save(readModel); // Update the read datastore
}
}
This event-driven approach allows the read model to be updated asynchronously. The command side can confirm success quickly, and the read side will eventually reflect the changes.
The most surprising true thing about CQRS is that the "query" side doesn’t necessarily have to be a traditional relational database. It can be a search engine like Elasticsearch, a document database like MongoDB, or even an in-memory data structure if your read needs are simple enough. The separation of concerns allows you to pick the best tool for the job for each side independently. For instance, writes might go to PostgreSQL for strong transactional guarantees, while reads might query Elasticsearch for full-text search capabilities on order descriptions.
The command side is all about ensuring consistency and correctness of your core business state. It enforces invariants. The query side is all about performance, flexibility, and delivering data in exactly the format your user interfaces or other services need, without being constrained by the write model’s structure. This often leads to a significant performance boost for read operations, as they can be served from highly optimized, specialized data stores.
A common pitfall is trying to keep the read model perfectly synchronized with the write model in real-time. CQRS embraces eventual consistency. You shouldn’t expect a read query to immediately reflect a write operation that just completed. The time lag between a write and its visibility in the read model is the "eventual" part. Understanding and accepting this delay is key to successfully implementing CQRS.
When you’re ready to explore how to handle complex scenarios like compensating transactions or managing optimistic concurrency in a CQRS architecture, you’ll start looking into event sourcing.