Skip to content

CQRS Pattern — Command Query Responsibility Segregation (2026)

DodaTech Updated 2026-06-20 7 min read

In this tutorial, you'll learn CQRS — Command Query Responsibility Segregation — splitting read and write operations into separate models for optimized performance and scalability. Why does this matter? In traditional CRUD, the same model handles both reads and writes, causing conflicts when reads need denormalized views and writes need normalized, validated structures. Real-world use: high-traffic systems at Amazon, EventStore, and CQRS-based e-commerce platforms use separate read models for product search and write models for order processing.

What Is CQRS?

CQRS separates operations into Commands (writes that change state) and Queries (reads that return state). A command says "do something" (with side effects) — PlaceOrder, UpdateProfile. A query asks "give me something" (no side effects) — GetOrderById, SearchProducts.

The separation can be at the method level (same class, different methods) or at the model level (completely different databases and services for reads vs writes).

graph TB
    Client --> Cmd[Command Handler]
    Client --> Qry[Query Handler]
    
    subgraph WriteSide[Write Model]
        Cmd --> Domain[Domain Logic]
        Domain --> EventStore[(Event Store / Write DB)]
    end
    
    subgraph ReadSide[Read Model]
        Qry --> ReadRepo[Read Repository]
        ReadRepo --> ReadDB[(Read DB / Cache)]
    end
    
    EventStore -.->|Project| ReadDB
    
    style WriteSide fill:#E74C3C,color:#fff
    style ReadSide fill:#2ECC71,color:#fff

How CQRS Works

Commands vs Queries

Commands are imperative (name them as orders): CreateOrder, CancelInvoice, ChangePassword. They return void or a confirmation — never data. Queries are interrogative: GetUserProfile, SearchProducts. They return data — never cause side effects.

from dataclasses import dataclass
from abc import ABC, abstractmethod

# --- Command ---
@dataclass
class CreateOrderCommand:
    user_id: str
    items: list[dict]
    shipping_address: Address

class CreateOrderHandler:
    def __init__(self, order_repo: OrderRepository, event_bus: EventBus):
        self._repo = order_repo
        self._bus = event_bus
    
    def handle(self, cmd: CreateOrderCommand) -> str:
        order = Order.create(cmd.user_id, cmd.items, cmd.shipping_address)
        self._repo.save(order)
        self._bus.publish(OrderCreated(order.id))
        return order.id  # OK to return the ID as a confirmation

# --- Query ---
@dataclass
class GetOrderQuery:
    order_id: str

class GetOrderHandler:
    def __init__(self, read_repo: OrderReadRepository):
        self._repo = read_repo
    
    def handle(self, query: GetOrderQuery) -> OrderView | None:
        return self._repo.find_by_id(query.order_id)

Separate Models

The write model is normalized — optimized for validation, consistency, and business rules. The read model is denormalized — optimized for fast queries with pre-joined data:

# Write model — normalized
class Order:
    def __init__(self):
        self._items: list[OrderItem] = []
        self._status: str = "pending"
    
    def add_item(self, product_id: str, quantity: int, price: float) -> None:
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        self._items.append(OrderItem(product_id, quantity, price))
    
    def submit(self) -> None:
        if not self._items:
            raise ValueError("Cannot submit empty order")
        self._status = "submitted"
        # Emit domain event for the read side

# Read model — denormalized
@dataclass
class OrderListView:
    order_id: str
    user_name: str
    item_count: int
    total_amount: float
    status: str
    created_at: datetime

Projections

A projection updates the read model whenever a write happens. This is where event sourcing integrates naturally — events from the write side build the read model:

class OrderProjection:
    def __init__(self, read_db: Database):
        self._db = read_db
    
    def on_order_created(self, event: OrderCreated) -> None:
        # Denormalize into the read table
        self._db.execute("""
            INSERT INTO order_views (order_id, user_name, item_count, total, status)
            VALUES ($1, $2, $3, $4, $5)
        """, event.order_id, event.user_name, len(event.items), event.total, "pending")
    
    def on_status_changed(self, event: StatusChanged) -> None:
        self._db.execute(
            "UPDATE order_views SET status = $1 WHERE order_id = $2",
            event.new_status, event.order_id
        )

Data Flow

sequenceDiagram
    participant C as Client
    participant W as Write API
    participant R as Read API
    participant WS as Write Store
    participant RS as Read Store
    
    C->>W: Command: CreateOrder
    W->>WS: Validate & Save
    WS-->>W: Confirmation
    W-->>C: Order ID
    
    C->>R: Query: GetOrderDetails
    R->>RS: Fetch denormalized
    RS-->>R: Data
    R-->>C: Response
    
    Note over WS,RS: Projection updates read store asynchronously
    WS->>RS: Projection

When NOT to Use CQRS

This is critical. CQRS adds significant complexity. Do NOT use it when:

  • Simple CRUD — one model works fine for basic create-read-update-delete
  • No read/write conflict — if reads and writes use the same shape, separate models add zero value
  • Strong consistency required — eventual consistency (the read model is behind the write model) is unacceptable
  • Small team — the extra code volume hurts velocity

Pros and Cons

Pros Cons
Optimized reads — read models designed for specific queries, not twisted by write concerns Eventual consistency — reads lag behind writes
Optimized writes — write models enforce business rules without query performance compromises Duplicate logic — validation logic sometimes duplicated
Independent scaling — scale read and write sides separately Complexity — projections, two models, sync mechanisms
Security — different permissions for commands and queries Increased storage — data stored twice (write + read)
Audit trail — commands naturally logged Learning curve — team must understand eventual consistency

Real-World Examples

Amazon (DynamoDB)

Amazon DynamoDB uses CQRS internally — write replicas accept updates with quorum, read replicas serve eventually consistent reads. This enables DynamoDB's single-digit-millisecond performance at global scale.

EventStore

EventStore is built on CQRS + Event Sourcing. Commands produce events, and projections build read models. The database itself enforces the separation.

E-Commerce Platforms

High-traffic e-commerce sites separate product catalog queries (denormalized for fast search) from order processing (normalized for transactional integrity, inventory validation, pricing rules).

When to Use CQRS

Use CQRS when:

  • Different read/write shapes — the write model is complex and normalized, reads need denormalized views
  • Performance isolation — reads must not be affected by write locks or vice versa
  • Team scaling — separate teams own reads and writes
  • Collaborative domains — Google Docs, Figma — where commands are fine-grained operations

FAQ

What is the difference between CQRS and CRUD?

CRUD uses the same model for all four operations. CQRS separates commands (Create, Update, Delete) from queries (Read) using different models, often different databases. CQRS is not a replacement for CRUD — it's an evolution for when CRUD's single-model approach creates conflicts.

Does CQRS require event sourcing?

No. CQRS and event sourcing are independent patterns that work well together. You can use CQRS with a traditional database — just separate the read and write models. Event sourcing is a complementary way to persist commands as events.

What is eventual consistency in CQRS?

When the write model updates, it takes time for the projection to update the read model. During that window, the read model returns stale data. This is acceptable for many cases (catalog searches, user profiles) but not for others (payment processing, inventory deductions).

Can I use CQRS within a single service?

Yes. CQRS is a model-level pattern, not a deployment pattern. You can have command handlers and query handlers in the same process, using different in-memory models or different database tables. Clean Architecture often uses CQRS at the use case level.

How do I handle validation in CQRS?

Commands are validated before execution — structural validation (required fields, data types) and business validation (sufficient inventory, valid user). Queries usually only need structural validation. Write validation is more expensive but ensures data integrity

Practice Questions

  1. What is the fundamental difference between a command and a query?

  2. How does CQRS solve the problem of conflicting read and write model shapes?

  3. What is a projection, and how does it keep the read model synchronized?

  4. Why is eventual consistency often acceptable for reads but not for writes?

  5. List three situations where CQRS adds unnecessary complexity.

Challenge

Take a standard CRUD controller for an order system. Split it into separate command handlers and query handlers. Create a denormalized read model that pre-joins user names and product details. Implement a simple projection that updates the read model when a write happens.

Real-World Task

Identify a high-traffic endpoint in your application where reads and writes compete. Separate the read path into its own handler with an optimized, denormalized query. Measure the performance improvement. If the read model becomes stale, determine whether eventual consistency is acceptable or if you need stronger guarantees.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro