On This Page
1The Problem It Solves2Pattern Structure
3When to Use4When Not to Use
5Trade-offs6Implementation Approach
7Anti-Patterns to Avoid8References

The Problem It Solves

A single data model that serves both reads and writes creates pressure in both directions. The write model must enforce business invariants — it needs consistency and transactional integrity. The read model must support flexible, high-performance queries — it needs denormalised views tailored to UI requirements. Trying to serve both with the same schema produces a model that does neither well. Complex joins slow writes; normalisation makes reads expensive.

Pattern Structure

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'Inter, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD CLIENT_C[Client Application] CLIENT_C --> CMD_TYPE{Operation Type} CMD_TYPE -->|Mutation| CMD[Command\nPlaceOrder, CancelOrder\nUpdateProfile] CMD_TYPE -->|Read| QRY[Query\nGetOrderHistory\nSearchProducts] CMD --> HANDLER[Command Handler\nValidate business rules\nEnforce invariants] HANDLER --> WRITE_DB[Write Model — Database\nNormalised, consistent\nACID transactions] WRITE_DB --> EVENT_C[Publish domain event\nOrderPlaced, OrderCancelled] EVENT_C --> PROJECTOR[Event Projector\nBuilds read model\nDenormalised views] PROJECTOR --> READ_DB[Read Model — Database\nDenormalised, optimised\nPer-query view] QRY --> READ_HANDLER[Query Handler\nNo business logic\nData retrieval only] READ_HANDLER --> READ_DB READ_DB --> RESULT([Query Result Returned]) style CLIENT_C fill:#4f8ef7,color:#fff style RESULT fill:#10b981,color:#fff style PROJECTOR fill:#e0f2fe style EVENT_C fill:#e0f2fe

When to Use

  • Systems where read and write patterns are fundamentally different in shape or scale
  • Domains with complex business rules that are hard to enforce alongside complex query requirements
  • Applications where read throughput significantly exceeds write throughput — read model can be scaled independently
  • Systems already using event sourcing — CQRS is a natural companion
  • Collaborative domains where multiple users modify the same aggregate concurrently

When Not to Use

  • Simple CRUD applications where the same data shape serves both reads and writes
  • Small-scale systems where the consistency window introduced by the read model projection is not acceptable
  • Teams without experience building and maintaining two separate data models
  • Domains with low data volume where a single well-indexed database handles both concerns efficiently

Trade-offs

Benefit Cost
Read and write sides scale independently Two data models to design, build, and maintain
Read model optimised per query — no joins at read time Eventual consistency between write and read models
Write model focuses solely on business invariants Event projectors add operational complexity
Simpler command handlers — one concern each Debugging spans write model, event bus, and read model

Implementation Approach

Define commands and queries as distinct types. Commands express intent: PlaceOrder, not SaveOrder. Queries are named for what they return: GetOrderSummary, not GetOrder. This distinction at the type level makes the segregation explicit in code.

Keep command handlers focused on invariants. A command handler validates the command, loads the aggregate, applies the business rule, saves the result, and publishes a domain event. Nothing else. No query logic, no read model concerns.

Build read model projections from events. When a domain event is published, a projector updates one or more read model tables. The projector denormalises — it joins data from multiple domain events into a single query-optimised view. If the read model becomes stale or corrupted, it can be rebuilt by replaying the event stream.

Accept eventual consistency in the read model. After a command completes, the read model may not reflect the change for a brief period while the projection processes the event. Design the UI to handle this — optimistic updates, loading states, or polling for confirmation are all valid strategies.

Anti-Patterns to Avoid

⚠ 1. CQRS on Simple CRUD

Applying CQRS to a straightforward resource management screen — create user, update profile, list users — where the write and read shapes are identical. Two models, two databases, event projectors, and eventual consistency for a feature that a single database table and a REST controller would serve perfectly.

Hover to see the fix ↻
↺ Correct Approach

Apply CQRS where the write and read concerns genuinely diverge. A useful test: if the read model would look identical to the write model, CQRS adds cost without benefit.

⚠ 2. Business Logic in Query Handlers

Putting business rule validation or state mutation into query handlers. Queries should return data and nothing else. When a query has side effects, the distinction between commands and queries breaks down and the pattern's benefits are lost.

Hover to see the fix ↻
↺ Correct Approach

Query handlers load from the read model and return. Any operation that changes state is a command. Command-query separation — no side effects in queries — is the non-negotiable invariant of CQRS.

Flowchart

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'Inter, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([User Interaction]) START --> TYPE{Intent} TYPE -->|Change state| CMD_D[Command\nExpresses intent to mutate\nPlaceOrder, CancelOrder] TYPE -->|Read state| QRY_D[Query\nExpresses need for data\nGetOrderHistory] CMD_D --> VALIDATE[Command Handler\nValidate business rules\nLoad aggregate\nApply invariants] VALIDATE --> WRITE[Write Database\nNormalised schema\nConsistency enforced] WRITE --> EVENT_D[Domain Event Published\nOrderPlaced, OrderCancelled\nImmutable fact about what happened] EVENT_D --> PROJECT[Projector\nBuilds denormalised view\nPer query shape\nEventually consistent] PROJECT --> READ_M[Read Database\nFlat denormalised schema\nOptimised per query pattern] QRY_D --> QUERY_H[Query Handler\nNo business logic\nNo state mutation\nLoad from read model only] QUERY_H --> READ_M READ_M --> RESPONSE([Data Returned to Client]) style START fill:#4f8ef7,color:#fff style RESPONSE fill:#10b981,color:#fff style PROJECT fill:#e0f2fe style EVENT_D fill:#e0f2fe

References

  1. Evans, Eric — Domain-Driven Design. Addison-Wesley, 2003.
  2. Young, Greg — CQRS Documents. cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
  3. Fowler, Martin — CQRS. martinfowler.com/bliki/CQRS
  4. Microsoft — CQRS Pattern. learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
Ascendion Engineering Knowledge Base ← Data Patterns