Command Query Responsibility Segregation (CQRS) doesn’t really separate "commands" and "queries" as much as it separates the models used to process them, and this is a distinction most people miss.
Let’s say you have an e-commerce system. A user browses products (queries), adds items to a cart (command), and then checks out (command).
Here’s a simplified view of how CQRS might look in practice, focusing on the data models.
Order Service (Command Side)
- Goal: Process incoming orders.
- Data Model: Optimized for writes. Think of a single, normalized
Orderstable with anOrderItemssub-table. It needs to be fast to insert and update. - Example Request:
POST /orders { "customerId": "cust_123", "items": [ {"productId": "prod_abc", "quantity": 2}, {"productId": "prod_xyz", "quantity": 1} ], "shippingAddress": "123 Main St" } - Internal Logic:
- Validate the order.
- Check inventory (this might involve a call to a separate inventory service).
- Persist the order to the
OrdersandOrderItemstables. - Publish an event:
OrderPlacedwith order details.
Inventory Service (Query Side - but also a command side for its own domain)
- Goal: Provide real-time inventory counts and process stock adjustments.
- Data Model: Optimized for reads. A denormalized
ProductInventoryview might be used, showingproductIdandavailableQuantity. This view is updated asynchronously. - Example Request (Query):
GET /inventory/products/prod_abc - Example Response:
{ "productId": "prod_abc", "availableQuantity": 50 } - Internal Logic (for updates):
- Listens for
OrderPlacedevents. - Decrements
availableQuantityforprod_abcandprod_xyzin itsProductInventorytable. - Publishes an
InventoryUpdatedevent.
- Listens for
Order Read Model (Query Side)
- Goal: Display order history to the customer quickly.
- Data Model: Highly denormalized, optimized for reads. A
CustomerOrderSummarytable might storeorderId,orderDate,totalAmount, and a list ofproductNamesandquantities. - How it’s populated: A background process (or event handler) listens for
OrderPlacedevents. When anOrderPlacedevent arrives, it queries theProductsread model (another separate read model, optimized for product details) to get product names and then creates or updates a record in theCustomerOrderSummarytable. - Example Request:
GET /customer/cust_123/orders - Example Response:
[ { "orderId": "ord_789", "orderDate": "2023-10-27T10:00:00Z", "totalAmount": 150.75, "items": [ {"productName": "Gadget Pro", "quantity": 2}, {"productName": "Widget Mini", "quantity": 1} ] } // ... more orders ]
The core idea here is that the Order Service (command side) writes to a normalized structure optimized for transactional integrity. The Order Read Model (query side) is a separate database/table, denormalized specifically to serve the UI’s need to display order summaries, and it’s populated asynchronously by listening to events from the command side. The Inventory Service demonstrates that even the "query side" can have its own command and read models.
The real power comes from tailoring each model to its specific job. The command model focuses on intent and consistency (e.g., "ensure this order is saved and inventory is deducted"). The query model focuses on performance and usability (e.g., "show me my orders with product names").
When you have a command that needs to update multiple read models, you’re likely to hit a scenario where your command side successfully processes the command and publishes an event, but one or more of your read models fail to update. This is often because the read model’s data source or the process consuming the event is temporarily unavailable or has a bug, leading to eventual consistency issues where the UI might not reflect the latest state immediately.