Introduction
CQRS (Command Query Responsibility Segregation) is a software design pattern that separates writes (commands) from reads (queries) into distinct models and often into separate data stores. The result: clearer responsibilities, better scaling for read-heavy systems, and a design that naturally fits event-driven and distributed architectures.
This article explains CQRS step-by-step and provides a concrete, real-world example using .NET Core Web APIs, MediatR style handlers, and a simple Angular client pattern. You’ll get code snippets, design decisions, trade-offs, and SEO-friendly headings to paste directly into your blog.
Why CQRS? Quick benefits
Scale reads and writes independently — tune each data store for its primary workload.
Optimized read models — denormalized views for fast queries (no costly joins).
Separation of concerns — simpler handlers for commands and queries.
Fits event-driven systems — easy to publish domain events and react with projections.
Enables advanced consistency strategies — eventual consistency, sagas for long-running transactions.
Trade-offs: added complexity, extra infrastructure (read DBs, message bus), eventual consistency challenges.
Step-by-step guide to implement CQRS
Step 1 — Evaluate whether you need CQRS
CQRS is not a silver bullet. Use it when:
You have heavy read traffic or complex read queries that hurt write performance.
You want to scale reads independently or serve different read models for different clients.
You need event-driven integrations or audit/history requirements.
Avoid CQRS for simple CRUD apps or small teams where complexity cost outweighs benefits.
Step 2 — Identify Commands and Queries
Commands (intent to change state):
PlaceOrder
,CancelOrder
,UpdateInventory
.Queries (read-only):
GetOrderSummary
,ListProducts
,SearchOrders
.
Commands are usually POST
/PUT
operations; queries map to GET
endpoints.
Step 3 — Design separate models
Write model (domain model) — authoritative, normalized, enforces business rules.
Read model (projection/view) — denormalized and optimized for queries (often stored in a different database or a different schema/table).
Step 4 — Implement command handlers
Commands go to handlers that validate, apply business logic, and persist to the write database. These handlers can publish domain events (in-process or to a message bus).
Step 5 — Publish domain events
After the write model changes, publish events (OrderPlaced
, InventoryReserved
) so projection handlers can update read models. Events should contain required projection data (or an aggregate id + version to fetch if necessary).
Step 6 — Build projections (read model updaters)
Projection handlers consume events and update the read store. This is where eventual consistency appears — the read model is updated after the write completes.
Step 7 — Implement query handlers
Query handlers read from the read model and return data shaped for the UI. Very fast — often a single table or precomputed view.
Step 8 — Handle consistency, retries and idempotency
Use idempotent event handlers.
Track processed event IDs to avoid double-processing.
Expose useful read-after-write guarantees (e.g., query the write DB directly for immediate read on the resource just created, or show a transient UI state).
Step 9 — Monitoring, testing and observability
Monitor event lag (time between write commit and projection update).
Test command handlers, event publication, and projection handlers separately.
Use tracing (for example, attach correlation IDs across command → event → projection flows).
Step 10 — Deploy and scale
Scale read DBs horizontally or use read replicas.
Command side may require stronger consistency and transactional guarantees.
Use message brokers (Kafka, RabbitMQ, Azure Service Bus) when crossing process or machine boundaries.
Real-world example: E‑commerce "Place Order" flow
We’ll show a compact example where placing an order is handled through CQRS. Simplified components:
Command:
PlaceOrderCommand
Command Handler: writes
Orders
in the write DB and publishesOrderPlacedEvent
Projection Handler: consumes
OrderPlacedEvent
and updatesOrderSummary
table in the read DBQuery:
GetOrderSummaryQuery
reads from read DB (fast, denormalized)
Note: This example uses in-process publishing (MediatR style). In production you may replace in-process events with a durable message bus to ensure cross-process delivery and reliability.
Data model (simplified)
Write DB (normalized) — tables: Orders
, OrderItems
, Inventory
Read DB (denormalized) — tables: OrderSummaries
(flattened, aggregated data optimized for UI)
SQL: write DB schema (simplified)
SQL: read DB schema (denormalized)
C# — Command, Handler, Event, Projection (MediatR-style)
Tip: Keep events lightweight and include only data needed to update projections. For bigger payloads, consider storing the full aggregate snapshot or giving the projection handler a reliable way to fetch required details.
API endpoints (example)
POST /api/commands/place-order
— receivesPlaceOrderCommand
DTO → returnsorderId
GET /api/queries/order-summary/{orderId}
— returnsOrderSummaryDto
Angular (very small snippet)
UI flow: after POST returns orderId
, call getOrderSummary(orderId)
. If projection lag is expected, show a transient "processing" state and retry or use WebSocket notifications when projection completes.