CQRS Pattern

CQRS Pattern

"CQRS stands for Command Query Responsibility Segregation. At its heart is the notion that you can use a different model to update information than the model you use to read information." — Martin Fowler, martinfowler.com/bliki/CQRS.html

Intent

CQRS (Command Query Responsibility Segregation) separates the write model (command side) from the read model (query side) as independent services with optionally separate data stores. Commands mutate state through a domain-rich write model; queries read from a denormalised read model optimised for fast retrieval. The two sides evolve independently and scale independently.

CQRS is independent of Event Sourcing. The write side can use a standard relational database. CQRS and Event Sourcing are independent patterns that compose naturally — Event Sourcing provides a natural event stream for CQRS projections — but neither requires the other.

The command side typically uses a command bus (a specialised Message-Router) to dispatch command objects to registered command handlers. The query side uses a query bus or direct repository access on the denormalised read model. This separation allows the read side to be tuned, cached, and replicated without affecting command handling logic.

When NOT to Use

CQRS adds significant complexity: two models, two data stores, eventual consistency, and a projection update mechanism. Fowler: "Most systems don't benefit from CQRS." Apply it only where the tradeoffs are justified.

  • CRUD systems — where reads and writes are symmetric in volume and domain complexity. Use a simple Repository-Pattern or Reporting Database instead.
  • Small teams or early-stage systems — the operational overhead of maintaining two models and handling eventual consistency outlasts the team's capacity to manage it.
  • No complex domain invariants — if command handling does not involve complex validation logic, a single model serves both reads and writes with no net loss.
  • Fowler: "CQRS should only be used on specific portions of a system and not the system as a whole."

Suitability threshold: Approximately 10x read/write scaling difference — read replicas with independent scaling cannot solve this with a shared model — OR genuinely complex command-side validation logic that is distorted by query optimisation requirements. If neither condition holds, CQRS adds complexity without benefit.

When to Use

  • ~10x read/write asymmetry where the query side must scale independently of the write side (e.g., reporting queries, public-facing dashboards).
  • Complex command-side validation with many domain invariants that a query-optimised model would distort.
  • Domains where eventual consistency is acceptable — the read model may be briefly stale after a command completes.
  • High-write domains where a denormalised read model avoids costly JOIN operations on every query.

How It Works

Command side: A controller or API endpoint constructs a command object (e.g., PlaceOrderCommand) and dispatches it through a Command Bus. The bus routes the command to the registered Command Handler. The handler validates the command against domain invariants, mutates the write model, and emits domain events.

Query side: A controller constructs a query object (e.g., GetOrderQuery) and dispatches it through a Query Bus. The handler reads from a denormalised read model — a separate database, materialised view, or cache — and returns the result directly.

Eventual consistency: The read model is updated asynchronously from domain events emitted by the command side. A query immediately after a command may return stale data. This is an inherent property of CQRS that must be communicated to API consumers and handled in the UI (e.g., optimistic updates).

Architecture Diagram

flowchart LR
    subgraph Command Side
        CC[Controller] -->|PlaceOrderCommand| CB[Command Bus]
        CB --> CH[Command Handler]
        CH --> WM[(Write Model DB)]
        CH -->|emit| DE[Domain Events]
    end

    DE -->|async propagation| EB[Event Bus]

    subgraph Query Side
        QC[Controller] -->|GetOrderQuery| QB[Query Bus]
        QB --> QH[Query Handler]
        QH --> RM[(Read Model DB)]
    end

    EB -->|project events| RM

    style Command Side fill:#e6f3ff,stroke:#4a90d9
    style Query Side fill:#e6ffe6,stroke:#4a9d4a

TypeScript Example

// CQRS Pattern — TypeScript (NestJS @nestjs/cqrs)
// Source: docs.nestjs.com/recipes/cqrs (verified via github.com/nestjs/docs.nestjs.com)
import { CommandHandler, ICommandHandler, QueryHandler, IQueryHandler } from '@nestjs/cqrs';
 
class PlaceOrderCommand { constructor(public readonly orderId: string) {} }
class GetOrderQuery    { constructor(public readonly orderId: string) {} }
 
@CommandHandler(PlaceOrderCommand)
export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
  async execute(cmd: PlaceOrderCommand): Promise<void> {
    // validate + mutate write model; emit domain events via EventBus
  }
}
 
@QueryHandler(GetOrderQuery)
export class GetOrderHandler implements IQueryHandler<GetOrderQuery> {
  async execute(query: GetOrderQuery): Promise<unknown> {
    // read from denormalised read model (separate DB or view)
    return { orderId: query.orderId, status: 'placed' };
  }
}
// Inject CommandBus / QueryBus in controllers to dispatch

Java Example

// CQRS Pattern — Java (Axon Framework)
// Source: axoniq.io/products/axon-framework; Baeldung axon-cqrs-event-sourcing guide
@Aggregate
public class OrderAggregate {
    @AggregateIdentifier private String orderId;
 
    @CommandHandler                  // write side: validate + emit event
    public OrderAggregate(PlaceOrderCommand cmd) {
        apply(new OrderPlacedEvent(cmd.getOrderId()));
    }
    @EventSourcingHandler            // rebuild state from events
    public void on(OrderPlacedEvent e) { this.orderId = e.getOrderId(); }
}
 
@Service
public class OrderQueryService {
    @QueryHandler                    // read side: query the read model
    public OrderView handle(GetOrderQuery query) {
        return readModelRepository.findById(query.getOrderId());
    }
}

Lineage Backward

  • CQS-Principle — CQRS is the service-level generalisation of method-level CQS. CQS says methods should be either commands or queries; CQRS promotes that separation to entire service models and data stores.
  • Message-Router — the command bus is a specialised Message Router that routes command objects to their registered handlers based on command type.
  • Domain-Events — the CQRS write side emits domain events after mutating the write model; these events propagate state changes to the read model and to downstream consumers.

Lineage Forward

  • Event-Sourcing-Pattern — Event Sourcing provides the event stream that CQRS projections consume to maintain the read model. The two patterns are independent but compose naturally.
  • Choreography-Saga-Pattern — choreography sagas (Phase 13) react to domain events emitted by the CQRS command side to coordinate distributed workflows.
PatternRelationship
CQS-PrincipleFoundation — CQS at method level generalised to service/data-store level
Event-Sourcing-PatternIndependent, composes well — ES write side produces the event stream CQRS projections consume
Message-RouterCommand bus ancestor — command bus is a specialised Message Router
Domain-EventsCommand side emits domain events to propagate write-side state changes
Choreography-Saga-PatternPhase 13 consumer — sagas react to CQRS command-side domain events
  • Vertical-Slice-Architecture — Command and query handlers in CQRS map naturally to vertical slices; each command or query is a self-contained slice owning its handler, DTOs, and data access.
  • REST-API-Design — CQRS command/query separation maps naturally to REST verb semantics; GET maps to queries, POST/PUT/DELETE map to commands
  • Distributed-Cache — the CQRS read model is frequently materialised into a distributed cache; Distributed-Cache covers the infrastructure layer (cache-aside strategy, eviction) while CQRS-Pattern covers the architectural read/write model separation
  • News-Feed-Design — the news feed timeline cache is a CQRS read model; post events project into per-user feed tables via fan-out workers — the case study applies CQRS at scale
  • Search-Autocomplete-Design — the autocomplete index is a CQRS read model: write path collects query logs; read path queries pre-aggregated projections — the two paths are independently scalable
  • Notification-System-Design — delivery receipt analytics use CQRS read/write separation: delivery events are the write path; analytics queries read from a separate projection
  • URL-Shortener-Design — click analytics are a CQRS read model: click events append to a log (write path); analytics queries aggregate from a separate projection

Sources