Overview
Command Query Responsibility Segregation (CQRS) separates operations that change state from operations that read state.
- A command expresses an intent to change the system.
- A query retrieves data without changing observable business state.
CQRS is a spectrum rather than one fixed architecture. A small implementation can use separate command and query handlers over the same database. A more advanced implementation can use different models, schemas, databases, deployment units, and scaling policies for reads and writes.
The pattern is useful when read and write concerns are meaningfully asymmetric:
- Writes enforce complex business rules and invariants.
- Reads need denormalized, presentation-specific shapes.
- Read traffic is much larger than write traffic.
- Reads and writes need independent scaling or storage technology.
- Multiple clients require different query projections.
- Contention or security requirements differ between the two paths.
CQRS adds costs:
- More code and concepts.
- Synchronization between models.
- Eventual consistency when stores are separated.
- Message delivery, duplicate handling, and replay concerns.
- More deployment, monitoring, and testing work.
It is usually unnecessary for straightforward CRUD applications where one model and one transactional database satisfy both reads and writes.
This topic matters in interviews because candidates must explain not only how CQRS works, but when the extra model separation creates enough value to justify its complexity. Strong answers distinguish CQRS from event sourcing and from simply using a mediator library.
Core Concepts
Commands and Queries
A command requests a state transition:
public sealed record PlaceOrder(
Guid OrderId,
Guid CustomerId,
IReadOnlyList<OrderLineRequest> Lines);
A query requests information:
public sealed record GetOrderDetails(Guid OrderId);
Commands should describe business intent:
PlaceOrder
ApproveExpense
CancelReservation
ChangeShippingAddress
They are clearer than generic data-edit commands:
UpdateOrder
SetStatus
PatchEntity
A command can be rejected because current state, authorization, or business rules do not permit the transition.
Command Handlers
A command handler coordinates one use case:
public sealed class PlaceOrderHandler
{
private readonly IOrderRepository orders;
private readonly IUnitOfWork unitOfWork;
public async Task<Guid> Handle(
PlaceOrder command,
CancellationToken cancellationToken)
{
var order = Order.Place(
command.OrderId,
command.CustomerId,
command.Lines);
orders.Add(order);
await unitOfWork.SaveChangesAsync(cancellationToken);
return order.Id;
}
}
The handler typically:
- Authorizes the operation.
- Loads required domain state.
- Invokes domain behavior.
- Persists changes atomically within one local transaction.
- Records events or outbox messages when integration is required.
- Returns a small acknowledgement or identifier.
It should not build large presentation models or expose persistence entities directly.
Query Handlers
A query handler can bypass the write-domain model and project directly into a read DTO:
public sealed class GetOrderDetailsHandler
{
private readonly OrderingDbContext db;
public Task<OrderDetails?> Handle(
GetOrderDetails query,
CancellationToken cancellationToken)
{
return db.Orders
.AsNoTracking()
.Where(order => order.Id == query.OrderId)
.Select(order => new OrderDetails(
order.Id,
order.Status,
order.CustomerName,
order.Lines.Sum(line => line.Quantity * line.UnitPrice)))
.SingleOrDefaultAsync(cancellationToken);
}
}
Query models should be optimized for consumers:
- Flat DTOs.
- Denormalized documents.
- Search indexes.
- Precomputed summaries.
- Client-specific projections.
Queries should not change business state. Operational telemetry such as access logs does not usually violate this rule, but hidden business side effects do.
Basic CQRS with One Database
The lowest-cost form uses:
Commands -> domain model -> shared database
Queries -> projections -> shared database
Benefits:
- Clear separation of business changes from reads.
- Query-specific DTOs without polluting domain entities.
- One transactional source of truth.
- No projection synchronization delay.
- Lower operational complexity than separate stores.
This is often the right starting point. Separate databases should be introduced only when independent scaling, storage, availability, or schema needs justify them.
Separate Read and Write Stores
Advanced CQRS can use:
Command
-> write model
-> write database
-> committed event/outbox
-> projection worker
-> read database
-> query
The write store is optimized for:
- Invariants.
- Transactions.
- Concurrency control.
- Normalized state.
- Aggregate updates.
The read store is optimized for:
- Query latency.
- Filtering and search.
- Denormalized views.
- Read replicas.
- Client-facing schemas.
The models are synchronized asynchronously, so reads may temporarily be stale.
Eventual Consistency
With separate stores, a successful command does not imply that every read projection is immediately current.
Example:
10:00:00.000 Order accepted by write model
10:00:00.020 Event published
10:00:00.180 Read projection updated
During the gap, a query can return the previous state.
The product must define acceptable behavior:
- Show a pending operation state.
- Return the command result directly for immediate confirmation.
- Poll until the projection reaches a known version.
- Route the user temporarily to the write model.
- Include a consistency token or expected version.
- Explain that processing continues asynchronously.
"Eventual consistency" is not an excuse for unspecified user experience.
Read-Your-Writes
Users often expect to see their own change immediately.
Possible strategies:
- Return enough data from the command to update the UI optimistically.
- Return an operation ID and expose a status resource.
- Include the committed aggregate version and wait until the read model reaches it.
- Read the authoritative write store for a limited workflow.
- Use synchronous projection only when latency and coupling are acceptable.
The system should not promise global strong consistency if it only provides session-level or operation-level read-your-writes behavior.
Materialized Views and Projections
A projection transforms authoritative changes into a query model:
public async Task Handle(
OrderPlaced message,
CancellationToken cancellationToken)
{
var view = new OrderSummaryDocument
{
OrderId = message.OrderId,
CustomerId = message.CustomerId,
Status = "Placed",
Total = message.Total,
Version = message.OrderVersion
};
await readStore.UpsertAsync(view, cancellationToken);
}
Projection handlers need:
- Idempotency.
- Version or ordering rules.
- Retry and dead-letter handling.
- Rebuild procedures.
- Monitoring for lag and failures.
- Schema migration strategy.
A projection is derived data. The system should know how to repair or rebuild it from an authoritative source.
Commands Are Not Events
A command is directed and imperative:
ReserveInventory for Order 123
An event states a fact that already occurred:
OrderPlaced for Order 123
Commands can be rejected. Events should not be phrased as requests that a consumer may reinterpret as a fact.
Using clear semantics improves ownership:
- The command target owns whether to perform an action.
- The event publisher owns the fact it emits.
- Event consumers independently decide how to react.
CQRS Does Not Require Messaging
Commands can be handled synchronously in process:
HTTP request -> command handler -> database transaction -> HTTP response
Messaging becomes useful when:
- Work is long-running.
- Load must be leveled.
- The caller need not wait.
- Services need loose temporal coupling.
- Projections or integrations update asynchronously.
Adding a mediator package or message bus does not by itself create meaningful CQRS. The architectural value comes from separating responsibilities and models.
CQRS Does Not Require Event Sourcing
CQRS and event sourcing are independent patterns.
CQRS with current-state persistence:
Write model -> current rows
Read model -> query projections
CQRS with event sourcing:
Write model -> append-only event stream
Read model -> projections built from events
Event sourcing can make projections rebuildable and preserve history, but adds:
- Event schema evolution.
- Replay behavior.
- Snapshot strategy.
- Event-store operations.
- More difficult deletion and privacy requirements.
- New debugging and testing techniques.
Do not introduce event sourcing merely because commands and queries are separated.
Transactional Outbox
When a command updates a database and publishes a message, two independent operations create a failure window:
database commit succeeds
message publish fails
The transactional outbox writes both domain changes and an outgoing-message record in the same local database transaction:
await using var transaction =
await db.Database.BeginTransactionAsync(cancellationToken);
order.Place();
db.OutboxMessages.Add(OutboxMessage.From(order.DomainEvents));
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
A separate publisher reads unsent outbox records and sends them to the broker. Publishing can occur more than once, so consumers still need idempotency.
Concurrency on the Write Side
Commands must protect invariants under concurrent updates.
Optimistic concurrency example:
public sealed class Order
{
public Guid Id { get; private set; }
public byte[] RowVersion { get; private set; } = [];
}
EF Core configuration:
builder.Property(order => order.RowVersion)
.IsRowVersion();
On conflict, the application can:
- Reject and ask the client to refresh.
- Reload and retry if the operation is safe.
- Merge using domain-specific rules.
- Serialize operations for one aggregate key.
CQRS does not remove concurrency; it gives the command model a focused place to manage it.
Validation and Authorization
Commands need:
- Structural validation.
- Authorization for the requested operation and resource.
- Business-rule validation in the domain model.
- Concurrency checks.
Queries need:
- Authorization and tenant filtering.
- Field-level data protection.
- Pagination and cost limits.
Read stores often contain denormalized sensitive data. Separating stores can reduce write exposure, but also creates more copies requiring access control, encryption, retention, and deletion handling.
Independent Scaling
CQRS is valuable when workloads differ:
95% reads
5% writes
The query side may use:
- Many replicas.
- Aggressive caching.
- Search infrastructure.
- Geographically distributed copies.
The write side may use:
- Fewer instances.
- Stronger consistency.
- Serialized aggregate updates.
- A relational transactional store.
Independent scaling matters only when measured workload asymmetry or reliability requirements justify the extra system.
Schema Evolution
Read models are consumer contracts. Evolve them deliberately:
- Add fields compatibly.
- Version event or message contracts.
- Run old and new projections in parallel.
- Backfill or replay data.
- Switch readers after verification.
- Retire old projections after the migration window.
Avoid coupling external consumers directly to internal write-domain types.
Testing Strategy
Test the write side with:
- Domain invariant tests.
- Command-handler integration tests.
- Authorization tests.
- Concurrency tests.
- Transaction and outbox tests.
Test the read side with:
- Projection tests.
- Query contract tests.
- Idempotency and replay tests.
- Stale and out-of-order event tests.
- Migration and rebuild tests.
End-to-end tests should verify the consistency window and user-visible pending behavior, not assume immediate projection updates.
When CQRS Makes Sense
CQRS is a strong candidate when:
- The domain has rich state transitions and invariants.
- Read shapes differ significantly from write models.
- Read and write traffic have different scale characteristics.
- Multiple read experiences need specialized projections.
- Eventual consistency is acceptable and can be explained.
- Independent teams own stable boundaries.
- Operational maturity exists for messaging and projections.
When CQRS Does Not Make Sense
Avoid or limit CQRS when:
- The application is simple CRUD.
- One database model serves both paths clearly.
- Strong immediate consistency is required everywhere.
- The team cannot operate brokers, projections, and repair workflows.
- Read and write loads are similar and modest.
- The pattern is being added only to follow a trend or framework template.
Start with separate code paths over one store. Increase physical separation only in response to evidence.
Common Mistakes
Common failures include:
- Treating every setter as a command.
- Returning domain entities from queries.
- Assuming commands always succeed.
- Calling any mediator-based architecture CQRS.
- Introducing a separate database before measuring need.
- Hiding eventual consistency from the product experience.
- Publishing messages outside the database transaction without an outbox.
- Assuming broker delivery is exactly once.
- Failing to rebuild or repair projections.
- Combining CQRS and event sourcing without a clear reason.
- Duplicating business rules in read projections.
- Forgetting authorization in denormalized read stores.
Best-Practice Decision Process
- Describe the concrete pain in the current read/write model.
- Separate command and query code paths first.
- Keep one database unless independent stores solve a measured problem.
- Model commands as business intent and queries as consumer-focused DTOs.
- Define concurrency and transaction boundaries on the write side.
- Define acceptable consistency delay and read-your-writes behavior.
- Use an outbox for reliable post-commit messaging.
- Make projections idempotent, observable, and rebuildable.
- Test stale reads, duplicates, ordering, and projection failure.
- Reassess whether the operational cost remains justified.