ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

CQRS Pattern

Separate read and write models to optimize for different access patterns. Covers command-query separation, read model projections, event-driven synchronization, eventual consistency, and the patterns that scale read-heavy and write-heavy workloads independently.

CQRS (Command Query Responsibility Segregation) separates the write model from the read model. Instead of a single database handling both reads and writes (and doing neither optimally), CQRS lets you optimize each independently. Writes go to a normalized model optimized for consistency. Reads come from denormalized models optimized for queries.


Why CQRS

Traditional CRUD:
  Single Model:  Users ←JOIN→ Orders ←JOIN→ Products
  
  Write: INSERT INTO orders (user_id, product_id, quantity, ...)
  Read:  SELECT o.*, u.name, p.title, p.price 
         FROM orders o 
         JOIN users u ON o.user_id = u.id
         JOIN products p ON o.product_id = p.id
         WHERE u.id = 123
         ORDER BY o.created_at DESC
  
  Problem: Same schema serves writes (normalized) AND reads (needs JOINs)
  3 JOINs on every read → slow at scale

CQRS:
  Write Model: Normalized tables (users, orders, products)
  Read Model:  Denormalized view (order_details_view)
    - Pre-joined, pre-computed
    - Instant reads, no JOINs
    - Updated asynchronously when write model changes

Architecture

             Commands (writes)              Queries (reads)
                  │                              │
                  ▼                              ▼
          ┌──────────────┐              ┌──────────────┐
          │ Command      │              │ Query        │
          │ Handler      │              │ Handler      │
          └──────┬───────┘              └──────┬───────┘
                 │                              │
                 ▼                              ▼
          ┌──────────────┐              ┌──────────────┐
          │ Write Model  │──Events───→  │ Read Model   │
          │ (PostgreSQL) │              │ (ElasticSearch│
          │ Normalized   │              │  or Redis)   │
          └──────────────┘              └──────────────┘

Write path: Validate → Apply → Store → Publish event
Read path: Query denormalized read model (fast, no JOINs)
Sync: Events update read model asynchronously

Implementation

# Command side: Handle writes
class CreateOrderCommand:
    user_id: str
    items: list[OrderItem]

class CreateOrderHandler:
    def handle(self, command: CreateOrderCommand):
        # Validate business rules
        user = self.user_repo.get(command.user_id)
        if not user.is_active:
            raise BusinessError("User account is inactive")
        
        # Create aggregate
        order = Order.create(
            user_id=command.user_id,
            items=command.items,
        )
        
        # Persist to write store
        self.order_repo.save(order)
        
        # Publish domain events
        self.event_bus.publish(OrderCreated(
            order_id=order.id,
            user_id=order.user_id,
            items=order.items,
            total=order.total,
        ))

# Query side: Handle reads
class OrderReadModel:
    """Denormalized read model updated by events."""
    
    def get_user_orders(self, user_id: str):
        # No JOINs! Pre-denormalized data
        return self.read_store.query(
            "SELECT * FROM order_details WHERE user_id = %s ORDER BY created_at DESC",
            user_id
        )

# Event handler: Sync read model
class OrderReadModelUpdater:
    def handle(self, event: OrderCreated):
        # Denormalize and store in read model
        user = self.user_service.get(event.user_id)
        
        for item in event.items:
            product = self.product_service.get(item.product_id)
            self.read_store.upsert("order_details", {
                "order_id": event.order_id,
                "user_id": event.user_id,
                "user_name": user.name,
                "product_name": product.name,
                "quantity": item.quantity,
                "price": product.price,
                "total": event.total,
                "created_at": event.timestamp,
            })

Anti-Patterns

Anti-PatternConsequenceFix
CQRS for simple CRUDUnnecessary complexityCQRS only when read/write patterns diverge significantly
Synchronous read model updatesWrite latency includes read model updateAsync events for read model sync
No eventual consistency strategyUI shows stale data confusingly”Your order is being processed” patterns
Shared database for read/writeDefeats the purposeSeparate stores, separate optimization
Too many read modelsMaintenance overheadOne read model per query pattern, not per screen

CQRS is powerful but not free. It adds complexity, eventual consistency, and operational overhead. Use it when your read and write patterns are fundamentally different — not for every service.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →