Introduction
As applications grow in complexity, managing data operations efficiently becomes increasingly challenging. Traditional architectures often use the same model for both reading and writing data. While this approach works well for smaller applications, it can become difficult to scale and maintain as business requirements evolve.
Consider an e-commerce platform that processes thousands of orders every minute while simultaneously serving millions of product searches. The requirements for writing order data and reading product information are often very different. Using the same model for both operations can create performance bottlenecks and increase application complexity.
This is where the CQRS (Command Query Responsibility Segregation) pattern becomes valuable. CQRS separates read operations from write operations, allowing each side to be optimized independently.
In this article, you'll learn how CQRS works, its architecture, benefits, challenges, implementation strategies, and best practices for modern applications.
What Is CQRS?
CQRS stands for:
Command Query Responsibility Segregation
The pattern separates:
Commands
Operations that modify data.
Examples:
Create Order
Update Product
Delete User
Process Payment
Queries
Operations that retrieve data.
Examples:
Get Order Details
Search Products
View Dashboard
Generate Reports
Instead of using a single model for both operations, CQRS creates separate models for reading and writing.
Traditional CRUD Architecture
Most applications start with a CRUD approach.
Application
↓
Single Model
↓
Database
The same model handles:
Create
Read
Update
Delete
While simple, this approach can become difficult to scale in complex systems.
CQRS Architecture
With CQRS:
Application
↓
Commands
↓
Write Model
↓
Database
Queries
↓
Read Model
↓
Read Database
Read and write operations are separated.
This allows each side to evolve independently.
Why Use CQRS?
CQRS addresses several common application challenges.
Independent Scaling
Read workloads often exceed write workloads.
Example:
10000 Reads
500 Writes
CQRS allows read systems to scale independently.
Improved Performance
Read models can be optimized specifically for queries.
Better Maintainability
Business logic becomes easier to organize.
Flexible Data Models
Read models and write models can have different structures.
Easier Integration with Event-Driven Systems
CQRS works naturally with event sourcing and messaging systems.
Understanding Commands
Commands represent actions that change system state.
Example:
CreateCustomer
A command contains:
Intent
Input data
Validation rules
Example:
public record CreateCustomerCommand(
string Name,
string Email
);
Commands do not return data.
They indicate that something should happen.
Command Handlers
Command handlers process commands.
Example:
public class CreateCustomerHandler
{
public async Task Handle(
CreateCustomerCommand command)
{
// Save customer
}
}
Responsibilities include:
Validation
Business rules
Data persistence
Handlers focus exclusively on write operations.
Understanding Queries
Queries retrieve information without changing data.
Example:
GetCustomerById
Query example:
public record GetCustomerQuery(
int CustomerId
);
Queries should never modify application state.
Query Handlers
Query handlers process read requests.
Example:
public class GetCustomerHandler
{
public async Task<CustomerDto>
Handle(
GetCustomerQuery query)
{
return customer;
}
}
Query handlers focus on data retrieval and presentation.
Read Models vs Write Models
One of CQRS's biggest advantages is model separation.
Write Model
Optimized for:
Business rules
Validation
Transactions
Example:
Customer Entity
Read Model
Optimized for:
Fast retrieval
Reporting
Search operations
Example:
Customer Dashboard View
Different models serve different purposes.
CQRS Workflow
A typical workflow:
Command Flow
User Request
↓
Command
↓
Command Handler
↓
Database
Query Flow
User Request
↓
Query
↓
Query Handler
↓
Read Database
The two paths remain independent.
CQRS with MediatR in ASP.NET Core
MediatR is commonly used to implement CQRS.
Install package:
dotnet add package MediatR
Register MediatR:
builder.Services
.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(
typeof(Program).Assembly);
});
MediatR simplifies command and query handling.
Command Example
Command:
public record CreateOrderCommand(
string Product,
decimal Price
);
Handler:
public class CreateOrderHandler
{
public async Task Handle(
CreateOrderCommand command,
CancellationToken token)
{
// Save order
}
}

Join the conversation! Your thoughts help the community grow.