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 Orders table with an OrderItems sub-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 Orders and OrderItems tables.
    • Publish an event: OrderPlaced with 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 ProductInventory view might be used, showing productId and availableQuantity. 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 OrderPlaced events.
    • Decrements availableQuantity for prod_abc and prod_xyz in its ProductInventory table.
    • Publishes an InventoryUpdated event.

Order Read Model (Query Side)

  • Goal: Display order history to the customer quickly.
  • Data Model: Highly denormalized, optimized for reads. A CustomerOrderSummary table might store orderId, orderDate, totalAmount, and a list of productNames and quantities.
  • How it’s populated: A background process (or event handler) listens for OrderPlaced events. When an OrderPlaced event arrives, it queries the Products read model (another separate read model, optimized for product details) to get product names and then creates or updates a record in the CustomerOrderSummary table.
  • 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.

Want structured learning?

Take the full Cqrs course →