CQRS Pattern — Command Query Responsibility Segregation (2026)
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
Related Concepts
- Event Sourcing — natural companion for CQRS
- Event-Driven Architecture — projections via events
- Clean Architecture — CQRS within use case layer
- Hexagonal Architecture — ports for commands and queries
- Saga Pattern — coordinating commands across services
- Repository Pattern — read/write repository separation
Practice Questions
What is the fundamental difference between a command and a query?
How does CQRS solve the problem of conflicting read and write model shapes?
What is a projection, and how does it keep the read model synchronized?
Why is eventual consistency often acceptable for reads but not for writes?
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