StackPractices
advanced By Mathias Paulenko

CQRS Pattern

Separate read and write operations into different models, optimizing each for their specific workload. A data pattern for scalable systems.

Topics: design

Overview

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read and write operations into distinct models. Instead of using a single data model for both queries and updates, CQRS uses a Command Model for writes and a Query Model for reads, each optimized for its specific workload.

When to Use

Use the CQRS Pattern when:

  • Read and write workloads have very different performance characteristics or scale independently
  • You need complex query capabilities (aggregation, filtering, search) without complicating the write model
  • Event sourcing is already in use, making read model projections natural
  • Different teams own reads vs. writes, and decoupling reduces coordination
  • Examples: analytics dashboards, e-commerce catalogs, social media feeds, reporting systems

Solution

Python

from dataclasses import dataclass, field
from typing import List, Dict
from datetime import datetime

# Write Model (Command Side)
@dataclass
class Order:
    order_id: str
    customer_id: str
    items: List[dict] = field(default_factory=list)
    status: str = "pending"
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())

class OrderCommandHandler:
    def __init__(self):
        self.orders: Dict[str, Order] = {}

    def create_order(self, order_id: str, customer_id: str) -> Order:
        order = Order(order_id=order_id, customer_id=customer_id)
        self.orders[order_id] = order
        return order

    def add_item(self, order_id: str, product: str, qty: int, price: float):
        order = self.orders[order_id]
        order.items.append({"product": product, "qty": qty, "price": price})

    def confirm(self, order_id: str):
        self.orders[order_id].status = "confirmed"

# Read Model (Query Side) — optimized for reads
@dataclass
class OrderSummary:
    order_id: str
    customer_id: str
    total: float
    item_count: int
    status: str

class OrderQueryHandler:
    def __init__(self, command_store: Dict[str, Order]):
        self.command_store = command_store

    def get_summary(self, order_id: str) -> OrderSummary:
        order = self.command_store[order_id]
        total = sum(i["qty"] * i["price"] for i in order.items)
        return OrderSummary(
            order_id=order.order_id,
            customer_id=order.customer_id,
            total=total,
            item_count=len(order.items),
            status=order.status
        )

    def list_by_customer(self, customer_id: str) -> List[OrderSummary]:
        return [
            self.get_summary(o.order_id)
            for o in self.command_store.values()
            if o.customer_id == customer_id
        ]

# Usage
commands = OrderCommandHandler()
commands.create_order("ORD-1", "CUST-1")
commands.add_item("ORD-1", "Laptop", 1, 999.99)
commands.add_item("ORD-1", "Mouse", 2, 29.99)
commands.confirm("ORD-1")

queries = OrderQueryHandler(commands.orders)
summary = queries.get_summary("ORD-1")
print(f"Order {summary.order_id}: ${summary.total:.2f} ({summary.item_count} items)")

JavaScript

// Write Model (Command Side)
class OrderCommandHandler {
  constructor() {
    this.orders = new Map();
  }

  createOrder(orderId, customerId) {
    this.orders.set(orderId, {
      orderId, customerId, items: [], status: "pending",
      createdAt: new Date().toISOString()
    });
  }

  addItem(orderId, product, qty, price) {
    this.orders.get(orderId).items.push({ product, qty, price });
  }

  confirm(orderId) {
    this.orders.get(orderId).status = "confirmed";
  }
}

// Read Model (Query Side)
class OrderQueryHandler {
  constructor(commandStore) {
    this.store = commandStore;
  }

  getSummary(orderId) {
    const order = this.store.get(orderId);
    const total = order.items.reduce((sum, i) => sum + i.qty * i.price, 0);
    return {
      orderId: order.orderId,
      customerId: order.customerId,
      total,
      itemCount: order.items.length,
      status: order.status
    };
  }

  listByCustomer(customerId) {
    return Array.from(this.store.values())
      .filter(o => o.customerId === customerId)
      .map(o => this.getSummary(o.orderId));
  }
}

// Usage
const commands = new OrderCommandHandler();
commands.createOrder("ORD-1", "CUST-1");
commands.addItem("ORD-1", "Laptop", 1, 999.99);
commands.confirm("ORD-1");

const queries = new OrderQueryHandler(commands.orders);
console.log(queries.getSummary("ORD-1"));

Java

import java.util.*;

// Write Model
class Order {
    String orderId;
    String customerId;
    List<Map<String, Object>> items = new ArrayList<>();
    String status = "pending";
    String createdAt = new Date().toString();
}

class OrderCommandHandler {
    private final Map<String, Order> orders = new HashMap<>();

    public void createOrder(String orderId, String customerId) {
        Order o = new Order();
        o.orderId = orderId;
        o.customerId = customerId;
        orders.put(orderId, o);
    }

    public void addItem(String orderId, String product, int qty, double price) {
        Map<String, Object> item = new HashMap<>();
        item.put("product", product);
        item.put("qty", qty);
        item.put("price", price);
        orders.get(orderId).items.add(item);
    }

    public void confirm(String orderId) {
        orders.get(orderId).status = "confirmed";
    }

    public Map<String, Order> getStore() { return orders; }
}

// Read Model
class OrderSummary {
    public String orderId;
    public String customerId;
    public double total;
    public int itemCount;
    public String status;
}

class OrderQueryHandler {
    private final Map<String, Order> store;

    public OrderQueryHandler(Map<String, Order> store) {
        this.store = store;
    }

    public OrderSummary getSummary(String orderId) {
        Order o = store.get(orderId);
        OrderSummary s = new OrderSummary();
        s.orderId = o.orderId;
        s.customerId = o.customerId;
        s.total = o.items.stream().mapToDouble(i ->
            (int)i.get("qty") * (double)i.get("price")).sum();
        s.itemCount = o.items.size();
        s.status = o.status;
        return s;
    }
}

// Usage
OrderCommandHandler commands = new OrderCommandHandler();
commands.createOrder("ORD-1", "CUST-1");
commands.addItem("ORD-1", "Laptop", 1, 999.99);
commands.confirm("ORD-1");

OrderQueryHandler queries = new OrderQueryHandler(commands.getStore());
System.out.println(queries.getSummary("ORD-1").total);

Explanation

CQRS separates two concerns:

  • Command Model: Optimized for writes — validates business rules, maintains invariants, processes state changes
  • Query Model: Optimized for reads — denormalized, indexed, often read from a separate database (e.g., Elasticsearch for search, Redis for fast lookups)

The two models are synchronized either synchronously (same transaction) or asynchronously (event-driven projections).

Variants

VariantDescriptionUse Case
Single DBSeparate models, same databaseSimple CQRS; lower complexity
Dual DBWrite to SQL, read from NoSQL/searchComplex queries; high read scale
Event Sourcing + CQRSEvents are source of truth; read models are projectionsAudit trails; temporal queries
API SegregationSeparate REST/GraphQL endpoints for commands and queriesMicroservices; team boundaries

What Works

  • Start simple — separate models within the same database before introducing dual storage
  • Use eventual consistency for read models when async projections are acceptable
  • Version your read models when the query schema changes
  • Monitor projection lag — ensure read models don’t fall too far behind writes
  • Keep commands small and focused — one command should do one thing

Common Mistakes

  • Applying CQRS to simple CRUD apps where a single model is sufficient
  • Allowing the read model to bypass business rules (validation belongs in commands)
  • Ignoring eventual consistency issues in async CQRS
  • Over-engineering with separate databases before proving the need
  • Not handling read model rebuilds when projection logic changes

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward. Use dependency inversion to break accidental coupling.
  • Tests break when the design changes: favor stable contracts over internal structure. Test observable behavior, not private helpers.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Common Production Pitfalls

  • Applying the pattern where no abstraction is needed, adding accidental complexity.
  • Letting the pattern leak into unrelated modules and blur ownership boundaries.
  • Over-engineering the first implementation instead of starting simple and measuring pain.
  • Skipping contract tests, so refactors silently break consumers.
  • Ignoring failure modes that the pattern does not cover.
  • Using the pattern as a default instead of choosing the right tool for the current scale.
  • Forgetting to document when to stop using the pattern and what replaces it.
  • Missing observability around the pattern’s performance and error propagation.

Frequently Asked Questions

Does CQRS require Event Sourcing?

No. CQRS can be used with any persistence model. Event Sourcing is often paired with CQRS because events make natural source data for read model projections, but they are independent patterns.

How do I keep read and write models in sync?

In synchronous CQRS, update both in the same transaction. In asynchronous CQRS, publish events after writes and have consumers rebuild the read model. Accept eventual consistency.

When should I avoid CQRS?

Avoid CQRS for simple CRUD applications, small teams, or when read/write ratios are balanced. See microservices architecture for distributed system patterns that often benefit from CQRS. The added complexity is only justified when the two sides have fundamentally different scaling or modeling needs.