📌 CQRS — Command Query Responsibility Segregation
CQRS is an architectural pattern that separates the read and write operations of a system into two distinct models. Instead of using a single data model to handle both queries and commands, CQRS splits them so each side can be optimized, scaled, and evolved independently. This seemingly simple idea has profound implications for how we design complex, high-performance distributed systems.
The term was coined by Greg Young, building on Bertrand Meyer's Command Query Separation (CQS) principle. While CQS operates at the method level — a method should either change state or return data, never both — CQRS elevates this to an architectural level, splitting entire models and sometimes even databases.
🔍 The Problem CQRS Solves
In a traditional CRUD-based architecture, you use the same data model for reading and writing. This works fine for simple applications, but as complexity grows, tensions emerge:
- Read/Write impedance mismatch: The shape of data you write is rarely the shape you read. An order creation involves validating inventory, applying discounts, and recording line items — but a dashboard query needs a flat, denormalized summary.
- Scalability bottleneck: Most systems are read-heavy (often 90%+ reads). A shared model forces both reads and writes through the same bottleneck.
- Complexity in domain models: Trying to serve both reads and writes from one model leads to bloated entities loaded with projections, DTOs, and view-specific logic.
- Performance trade-offs: Optimizing a database for fast writes (normalized, indexed for inserts) conflicts with optimizing for fast reads (denormalized, materialized views).
- Security and authorization: Read and write operations often have different security requirements that are awkward to enforce on a single model.
⚙️ How CQRS Works — Command Model vs Query Model
At its core, CQRS splits your application into two sides:
Command Side (Write Model)
The command side handles all state-changing operations. Commands are imperative — they represent an intent to do something: PlaceOrder, TransferFunds, UpdateProfile. The command model is optimized for business rule validation, consistency, and transactional integrity.
- Commands are processed by Command Handlers
- The write model contains the full domain logic
- It enforces invariants and business rules
- The write database is normalized for consistency
Query Side (Read Model)
The query side handles all data retrieval. Queries are non-mutating — they return data without changing state: GetOrderSummary, ListRecentTransactions. The read model is optimized for fast retrieval and presentation.
- Queries are processed by Query Handlers
- The read model is denormalized, often pre-computed
- It can use materialized views, caches, or search indexes
- The read database is optimized for query patterns
The Mediator Pattern
In most implementations, a mediator (or dispatcher) routes commands and queries to their respective handlers. Libraries like MediatR in C# or Axon Framework in Java make this straightforward.
// C# — Command and Query with MediatR
// Command
public record PlaceOrderCommand(string CustomerId, List<OrderItem> Items) : IRequest<Guid>;
public class PlaceOrderHandler : IRequestHandler<PlaceOrderCommand, Guid>
{
private readonly IOrderRepository _repo;
private readonly IEventBus _eventBus;
public PlaceOrderHandler(IOrderRepository repo, IEventBus eventBus)
{
_repo = repo;
_eventBus = eventBus;
}
public async Task<Guid> Handle(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Items);
order.Validate();
await _repo.SaveAsync(order, ct);
await _eventBus.PublishAsync(new OrderPlacedEvent(order.Id, cmd.CustomerId));
return order.Id;
}
}
// Query
public record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;
public class GetOrderSummaryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>
{
private readonly IReadDbContext _readDb;
public GetOrderSummaryHandler(IReadDbContext readDb)
{
_readDb = readDb;
}
public async Task<OrderSummaryDto> Handle(GetOrderSummaryQuery query, CancellationToken ct)
{
return await _readDb.OrderSummaries
.FirstOrDefaultAsync(o => o.OrderId == query.OrderId, ct);
}
}
🧩 Read/Write Separation with Different Databases
One of the most powerful aspects of CQRS is that the read and write sides can use entirely different storage technologies. This is not required — you can start with a single database — but it unlocks significant optimization opportunities.
| Aspect | Write Store | Read Store |
|---|---|---|
| Database Type | SQL Server, PostgreSQL | Elasticsearch, Redis, DynamoDB |
| Schema | Normalized (3NF) | Denormalized, per-view |
| Optimization | Write throughput, ACID compliance | Query speed, low latency |
| Scaling | Vertical or limited horizontal | Horizontally scalable replicas |
| Consistency | Strong consistency | Eventual consistency |
The synchronization between write and read stores happens through domain events or change data capture (CDC). When a command modifies data, an event is published, and projections (also called denormalizers) update the read store accordingly.
// C# — Projection that updates the read model
public class OrderSummaryProjection : IEventHandler<OrderPlacedEvent>
{
private readonly IReadDbContext _readDb;
public OrderSummaryProjection(IReadDbContext readDb)
{
_readDb = readDb;
}
public async Task HandleAsync(OrderPlacedEvent evt)
{
var summary = new OrderSummaryDto
{
OrderId = evt.OrderId,
CustomerId = evt.CustomerId,
Status = "Placed",
TotalAmount = evt.TotalAmount,
ItemCount = evt.ItemCount,
PlacedAt = evt.OccurredAt
};
await _readDb.OrderSummaries.InsertAsync(summary);
}
}
🔗 Integration with Event Sourcing
CQRS and Event Sourcing are often mentioned together, but they are independent patterns. You can use CQRS without event sourcing and vice versa. However, they complement each other exceptionally well.
With Event Sourcing, instead of storing the current state of an entity, you store the sequence of events that led to that state. The write side appends events to an event store, and the read side builds projections from those events.
// Event-sourced aggregate with CQRS
public class OrderAggregate
{
private readonly List<IDomainEvent> _pendingEvents = new();
public Guid Id { get; private set; }
public string Status { get; private set; }
public decimal Total { get; private set; }
public void PlaceOrder(string customerId, List<OrderItem> items)
{
if (items == null || !items.Any())
throw new DomainException("Order must have at least one item.");
var total = items.Sum(i => i.Price * i.Quantity);
Apply(new OrderPlacedEvent(Id, customerId, total, items.Count));
}
public void CancelOrder(string reason)
{
if (Status == "Cancelled")
throw new DomainException("Order is already cancelled.");
Apply(new OrderCancelledEvent(Id, reason));
}
private void Apply(IDomainEvent evt)
{
When(evt);
_pendingEvents.Add(evt);
}
private void When(IDomainEvent evt)
{
switch (evt)
{
case OrderPlacedEvent e:
Id = e.OrderId;
Status = "Placed";
Total = e.TotalAmount;
break;
case OrderCancelledEvent e:
Status = "Cancelled";
break;
}
}
}
The event store becomes the single source of truth for the write side. Read-side projections subscribe to the event stream and build optimized views. If a projection becomes corrupted or a new view is needed, you can replay the event stream to rebuild it from scratch — one of the most powerful benefits of combining these patterns.
⏳ Handling Eventual Consistency
When read and write models are separated — especially across different databases — eventual consistency becomes a reality you must design for. After a command executes, the read model may take milliseconds to seconds to reflect the change.
Strategies for handling this include:
- Optimistic UI updates: Update the UI immediately based on the command's success, without waiting for the read model. Most modern SPAs use this approach.
- Polling or server-sent events: After a command, poll the read model or use WebSockets/SSE to push updates when the projection catches up.
- Read-your-own-writes: After a write, route subsequent reads for that user through the write store temporarily, or use a session-level cache.
- Causal consistency tokens: Include a version or timestamp token with the command response. The client sends it with the next query, and the read side waits until it has processed up to that version.
- Accept it in the UX: Display "Your order is being processed" instead of immediately showing the order. Many real-world systems — bank transfers, e-commerce order placement — already work this way.
✅ Pros and Cons of CQRS
| Pros | Cons |
|---|---|
| Independent scaling of reads and writes | Increased architectural complexity |
| Optimized read models for each query pattern | Eventual consistency challenges |
| Cleaner domain models focused on behavior | More code to maintain (two models, projections) |
| Better separation of concerns and team ownership | Debugging is harder across async boundaries |
| Enables event sourcing and temporal queries | Overkill for simple CRUD applications |
| Flexible technology choices per side | Requires infrastructure for event propagation |
| Enhanced security — restrict write access separately | Team must understand distributed systems concepts |
🚦 When to Use CQRS — and When NOT To
Use CQRS When:
- Your system has a high read-to-write ratio and reads need to be extremely fast
- Read and write models have significantly different shapes
- You need to scale reads and writes independently
- You are building a complex domain with rich business rules (DDD context)
- You need multiple read representations of the same data (dashboard, search, reporting)
- You want to integrate with event sourcing for audit trails and temporal queries
- Different teams own the read and write paths
Do NOT Use CQRS When:
- Your application is a simple CRUD with straightforward data access
- Your domain is not complex enough to justify the overhead
- Your team is not experienced with distributed systems and eventual consistency
- You have strict real-time consistency requirements that cannot tolerate any delay
- The project has a tight deadline and limited resources for infrastructure
🌍 Real-World Examples
E-Commerce: Order Management
A large e-commerce platform processes thousands of orders per minute. The write side uses a relational database (PostgreSQL) with strict transactional guarantees for order placement, payment processing, and inventory deduction. The read side uses Elasticsearch for the product catalog search, Redis for shopping cart views, and a denormalized SQL database for order history dashboards. Events like OrderPlaced, PaymentConfirmed, and ItemShipped flow through Kafka to keep all read models in sync.
Banking: Transaction Processing
A banking system uses CQRS to separate the transactional ledger (write side) from account balance views and statement generation (read side). The write model enforces strict invariants — overdraft limits, fraud checks, regulatory compliance. The read model provides instant balance lookups, monthly statements, and analytics dashboards. Event sourcing on the write side provides a complete, immutable audit trail of every financial transaction — a regulatory requirement. Learn more about designing financial systems in our guide on distributed transactions.
Social Media: News Feed
A social platform separates the write path (posting content, liking, commenting) from the read path (rendering personalized feeds). When a user posts, the write model validates and persists the post. Asynchronous fan-out processes then update precomputed feed caches for each follower. The read model is heavily cached and denormalized, served from a CDN-backed store, while the write model uses a strongly consistent database.
🛠️ Implementation Patterns
Pattern 1: Single Database, Separate Models
The simplest starting point. Use one database but maintain separate read and write models in code. Write operations go through domain entities with full validation. Read operations use lightweight DTOs or raw SQL queries against views. This gives you the code-level benefits of CQRS without the operational complexity of multiple databases.
Pattern 2: Separate Databases with Async Sync
The write database (e.g., PostgreSQL) publishes events via a message broker (Kafka, RabbitMQ, Azure Service Bus). Projection handlers consume events and update the read database (e.g., Elasticsearch, MongoDB). This enables full independent scaling and technology optimization. Explore message queue patterns for reliable event delivery.
Pattern 3: CQRS with Event Sourcing
The write side appends events to an event store (EventStoreDB, Marten, or a custom implementation on top of a relational database). The read side subscribes to the event stream and builds projections. This is the most powerful but also the most complex pattern. It provides full audit trails, temporal queries, and the ability to rebuild read models at any time.
Pattern 4: CQRS in Microservices
Each microservice owns its data and implements CQRS internally. Cross-service queries are handled by an API Gateway or a dedicated query service that aggregates data from multiple read stores. This pairs well with the Saga pattern for distributed commands that span services.
🧰 Useful Tools and Frameworks
If you are exploring CQRS for system design interviews or real implementations, these resources can help:
- MediatR (.NET): Lightweight mediator library for dispatching commands and queries
- Axon Framework (Java): Full-featured CQRS and event sourcing framework
- EventStoreDB: Purpose-built database for event sourcing
- Marten (.NET): Document database and event store built on PostgreSQL
- Use the System Design Checklist to evaluate whether CQRS is right for your architecture
- Practice with the Architecture Diagram Builder to visualize CQRS data flows
❓ Frequently Asked Questions
Q: Is CQRS the same as Event Sourcing?
No. CQRS is about separating read and write models. Event Sourcing is about storing state changes as a sequence of events instead of the current state. They are complementary but independent patterns. You can use CQRS with a traditional relational database on both sides, or you can use event sourcing without separating your read and write models. They are most powerful when combined, as event sourcing provides a natural mechanism for publishing events that update read projections.
Q: Does CQRS require two separate databases?
No. You can implement CQRS with a single database by maintaining separate code-level models for reads and writes. You might use the same PostgreSQL instance but read through denormalized views while writing through normalized tables. Separate databases are an optimization, not a requirement. Start simple and split when the performance or scaling needs justify it.
Q: How do I handle eventual consistency in the UI?
The most common approach is optimistic UI updates — update the interface immediately when the command succeeds, without waiting for the read model to catch up. You can also use polling, WebSockets, or causal consistency tokens. The key insight is that many real-world processes are already eventually consistent (bank transfers, email delivery), so users are more tolerant of slight delays than developers assume.
Q: Can I apply CQRS to just part of my system?
Absolutely, and this is the recommended approach. Apply CQRS only to the bounded contexts where the read/write complexity justifies it. A user profile service might be simple CRUD, while the order management service benefits greatly from CQRS. Greg Young himself emphasizes that CQRS should be applied selectively, not system-wide. See our Domain-Driven Design guide for more on bounded contexts.
Q: What is the performance impact of CQRS?
When implemented correctly, CQRS significantly improves read performance because the read model is tailored exactly to your query needs — no joins, no computed fields at query time. Write performance remains similar or improves slightly because the write model does not carry read-optimization overhead. The trade-off is increased write latency if you factor in the time for projections to update the read store, but this is asynchronous and does not block the write path.