← All topics
🧩 Design Patterns

CQRS

Separating reads from writes — and knowing it doesn't require separate databases.

Command Query Responsibility Segregation: separate models (and often separate code paths) for writes (Commands — change state, don't return data) and reads (Queries — return data, don't change state). This is Clean Code's Command-Query Separation applied at the architecture level, not just per-method.

Important nuance to state clearly, because it's the #1 CQRS misconception: CQRS does not require two separate databases or event sourcing. The simplest, very common form is "same database, different code paths": write side uses your rich domain model + EF Core change tracking; read side uses lean, AsNoTracking() projection queries straight into DTOs, often bypassing the domain model entirely for speed. Full CQRS with separate read/write stores (and eventual consistency between them, usually synced via events) is a heavier architectural commitment you'd reach for under real read/write load or model divergence — not a default.

Combined with MediatR: IRequest queries and IRequest (no response, or a simple result) commands, routed through the same mediator pipeline, is the standard .NET implementation shape.

// Command — changes state, minimal/no return data
public record PlaceOrderCommand(string CustomerId, List<OrderLine> Lines) : IRequest<Guid>;

// Query — reads only, own lean projection
public record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;

Flashcards (3)

What is the #1 misconception about CQRS you should correct if asked?
tap to reveal answer
That CQRS requires two separate databases or event sourcing. The simplest and most common form uses one database with different code paths: a rich tracked domain model for writes, lean AsNoTracking() DTO projections for reads.
What's the practical read-side optimization CQRS naturally leads to with EF Core?
tap to reveal answer
Bypassing the full domain model for reads and projecting straight from the query into a DTO with AsNoTracking() — no change tracking overhead, no loading a full aggregate just to read a few fields.
When would 'full' CQRS with separate read and write stores actually be justified?
tap to reveal answer
Under genuinely asymmetric read/write load, or when the read model's shape diverges significantly from the write/domain model (e.g. heavily denormalized reporting views) — it's a deliberate scaling/complexity tradeoff, not a default starting point.