CQRS Pattern
Separate read and write models for optimized query and command paths. Covers CQRS architecture, event sourcing integration, read model projections, eventual consistency trade-offs, and when CQRS is worth the complexity.
Command Query Responsibility Segregation (CQRS) splits your application into two separate models: one optimized for writes (commands) and another optimized for reads (queries). Instead of forcing a single database schema to handle both complex writes and fast reads, each side gets a model tailored to its requirements.
Why CQRS
Traditional (single model):
Write: INSERT INTO orders (...) VALUES (...)
Read: SELECT o.*, c.name, p.name, SUM(oi.quantity * oi.price)
FROM orders o
JOIN customers c ON ...
JOIN order_items oi ON ...
JOIN products p ON ...
WHERE o.status = 'active'
GROUP BY ...
Problem: The read query is complex, slow, and fights with write locks.
CQRS (separate models):
Write model: Normalized, optimized for data integrity
INSERT INTO orders (...) VALUES (...)
Read model: Denormalized, optimized for query speed
SELECT * FROM order_summaries WHERE status = 'active'
(Pre-computed, no joins needed)
Architecture
┌──────────────────┐
│ API Gateway │
└────────┬─────────┘
│
┌────────────┴────────────┐
│ │
┌─────────▼──────────┐ ┌────────▼────────────┐
│ Command Service │ │ Query Service │
│ (write path) │ │ (read path) │
└─────────┬──────────┘ └────────▲────────────┘
│ │
┌─────────▼──────────┐ ┌────────┴────────────┐
│ Write Database │───▶│ Read Database │
│ (PostgreSQL) │ │ (Elasticsearch, │
│ Normalized │ │ Redis, DynamoDB) │
└────────────────────┘ └─────────────────────┘
│
Domain Events
│
┌─────────▼──────────┐
│ Event Bus │
│ (Kafka, RabbitMQ) │
└────────────────────┘
Command Side
class CreateOrderCommand:
customer_id: str
items: list[OrderItem]
shipping_address: Address
class OrderCommandHandler:
def handle(self, cmd: CreateOrderCommand):
# Validate business rules
customer = self.customer_repo.get(cmd.customer_id)
if not customer.is_active:
raise BusinessRuleViolation("Customer is inactive")
# Create aggregate
order = Order.create(
customer_id=cmd.customer_id,
items=cmd.items,
shipping_address=cmd.shipping_address
)
# Persist (write-optimized storage)
self.order_repo.save(order)
# Publish domain events
self.event_bus.publish(OrderCreatedEvent(
order_id=order.id,
customer_id=order.customer_id,
total=order.total,
items=order.items,
created_at=order.created_at
))
Query Side (Projections)
class OrderProjection:
"""Listens to events and builds read-optimized views."""
def handle_order_created(self, event: OrderCreatedEvent):
self.read_db.upsert("order_summaries", {
"order_id": event.order_id,
"customer_name": self.lookup_customer_name(event.customer_id),
"item_count": len(event.items),
"total": event.total,
"status": "pending",
"created_at": event.created_at
})
def handle_order_shipped(self, event: OrderShippedEvent):
self.read_db.update("order_summaries",
{"order_id": event.order_id},
{"status": "shipped", "shipped_at": event.shipped_at}
)
class OrderQueryService:
def get_active_orders(self, customer_id: str):
# Simple, fast query — no joins
return self.read_db.find("order_summaries", {
"customer_id": customer_id,
"status": {"$ne": "completed"}
})
When to Use CQRS
Use CQRS when:
✅ Read and write patterns are fundamentally different
✅ Complex reads require multiple joins/aggregations
✅ Read scale is 10-100x write scale
✅ Different teams own read vs write paths
✅ You need different storage optimized for each path
Don't use CQRS when:
❌ Simple CRUD with straightforward queries
❌ Strong consistency required for all reads
❌ Small team, simple domain
❌ Adding complexity without clear benefit
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| CQRS for simple CRUD | Unnecessary complexity | Only use when read/write asymmetry exists |
| Ignoring eventual consistency | UI shows stale data, user confusion | Optimistic UI, read-your-own-writes |
| Projections without error handling | Read model drift from write model | Dead letter queue, replay capability |
| Single database for both | No benefit, just extra code | Separate storage optimized for each path |
| No event replay capability | Cannot rebuild read models | Store events immutably, support replay |
CQRS is a powerful pattern that shines in specific situations — high read-write asymmetry, complex domains, and scale requirements. Applied to simple CRUD applications, it is unnecessary complexity.