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

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-PatternConsequenceFix
CQRS for simple CRUDUnnecessary complexityOnly use when read/write asymmetry exists
Ignoring eventual consistencyUI shows stale data, user confusionOptimistic UI, read-your-own-writes
Projections without error handlingRead model drift from write modelDead letter queue, replay capability
Single database for bothNo benefit, just extra codeSeparate storage optimized for each path
No event replay capabilityCannot rebuild read modelsStore 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.

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 →