SuperAPI AI Directory

cqrs

Coding & development

View repository

Updated 10-15 21:49

About this skill

CQRS (Command Query Responsibility Segregation)

CQRS is a pattern that separates read and update operations for a data store. Instead of using a single model for both, you have a Command Model (Write) and a Query Model (Read).

When to Use

  • Asymmetric Read/Write Workloads: Systems where reads outnumber writes by 100:1 or 1000:1 and require independent horizontal scaling.
  • Complex Reporting & Search Views: Aggregating data across multiple aggregates into denormalized read stores (Elasticsearch, Read Replicas).
  • Collaboration & High Concurrency: Optimizing write performance and transactional consistency while delivering instant query responses.
  • Pairing with Event Sourcing: When state changes are captured as events and read models are asynchronously projected from event streams.

Quick Start

// Command Side (Write) - Optimized for Integrity
class CreateOrderCommand { ... }
class OrderAggregate {
    create(cmd: CreateOrderCommand) {
        // Validate business rules
        // Save to DB (Normalized / Event Store)
    }
}

// Query Side (Read) - Optimized for Speed
class GetOrderSummaryQuery { ... }
class OrderSummaryProjector {
    // Listens to "OrderCreated" event
    on(event) {
        // Update a flat "Read DB" (e.g., ElasticSearch, Redis, Document DB)
        // optimized for the specific UI view
    }
}

Core Concepts

Separation of Command and Query Models

Commands represent intent to mutate state (void return or ID); Queries request data without side effects:

Client
  ├── Command: POST /orders (CreateOrderCommand) ──→ Command Handler ──→ Write DB (Postgres)
  │                                                                           │ (CDC / Events)
  └── Query:   GET /orders/summary (OrderSummary) ←── Read Handler   ←── Read DB (Elastic/Redis)

Command Handlers with Explicit Domain Mutex

Validates permissions and business invariants before committing writes:

// commands/cancel-order.command.ts
export interface CancelOrderCommand {
  orderId: string;
  reason: string;
  requestedByUserId: string;
}

export class CancelOrderCommandHandler {
  constructor(private readonly orderRepo: OrderRepositoryPort) {}

  async handle(cmd: CancelOrderCommand): Promise<void> {
    const order = await this.orderRepo.getById(cmd.orderId);
    if (!order) throw new Error("Order not found");
    order.cancel(cmd.reason, cmd.requestedByUserId);
    await this.orderRepo.save(order);
  }
}

Materialized Read Projections

Updates optimized denormalized read views in response to domain events:

// projections/order-dashboard.projection.ts
export class OrderDashboardProjection {
  async onOrderCancelled(event: OrderCancelledEvent): Promise<void> {
    await redis.hset(`dashboard:order:${event.orderId}`, {
      status: "CANCELLED",
      cancelledAt: event.timestamp,
      cancellationReason: event.reason,
    });
  }
}

Common Patterns

Read-Model Projection with Event Handlers

Problem: Queries require aggregated data from multiple write models, causing slow and complex database joins.

Solution: Use an asynchronous projector to update a de-normalized, query-optimized document or key-value store:

interface OrderCreatedEvent {
  orderId: string;
  customerId: string;
  total: number;
  createdAt: string;
}

class OrderReadModelProjector {
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    await searchIndex.upsert("orders_summary", event.orderId, {
      id: event.orderId,
      customerId: event.customerId,
      totalAmount: event.total,
      placedAt: event.createdAt,
      status: "PLACED",
    });
  }
}

Best Practices

Do:

  • Make Queries Completely Side-Effect Free: Ensure queries never mutate database state or trigger transactional locks.
  • Embrace Eventual Consistency: Design client user experiences to anticipate small projection delays (optimistic UI updates).
  • Optimize Read Models for the UI: Structure read tables/documents to match the exact view model needed by frontend clients.
  • Use Dedicated Read Replicas: Direct query handlers to read replicas or fast key-value caches to protect the write primary.

Don't:

  • Apply CQRS globally to an entire system: Apply CQRS only to bounded contexts with complex requirements; simple CRUD benefits from standard MVC.
  • Share entities between Command and Query pipelines: Keep query handlers completely independent of domain aggregate classes.
  • Let read projections write back to the write store: Keep the data pipeline unidirectional.

Troubleshooting

Error Cause Solution
Stale Data Lag between Command execution and Query update. UI design updates (spinners/optimistic updates); Check projector lag.
Complexity Over-engineering. Revert to simple CRUD if the domain doesn't warrant separation.

References