Design Patterns & Practices  

Command Query Responsibility Segregation

1. What is CQRS?

CQRS stands for Command Query Responsibility Segregation.
It means separating the read (queries) from the write (commands) side of your application.

  • Command → modifies data (Create, Update, Delete)

  • Query → retrieves data (Read-only)

Instead of a single model handling both reads and writes, CQRS uses two separate models .

🔹 2. Why use CQRS?

  • Scalability → Read and write workloads can scale independently

  • Performance → Queries can be optimized (denormalized DB, caching) without affecting writes

  • Flexibility → Supports event sourcing and async messaging

  • Separation of Concerns → Clearer design between commands and queries

🔹 3. Typical Architecture
Client → API → Command Side (Write model) → Database (writes)

↘︎ API → Query Side (Read model) → Database (reads)

  • The write side updates the domain model and stores events/state.

  • The read side provides fast, possibly denormalized access to data.

  • In distributed systems, they may have separate databases.

Command (Write)

  
    public record CreateOrderCommand(string ProductName, int Quantity) : IRequest<int>;

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly AppDbContext _db;
    public CreateOrderHandler(AppDbContext db) => _db = db;

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = new Order { ProductName = request.ProductName, Quantity = request.Quantity };
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);
        return order.Id;
    }
}
  

Query (Read)

  
    public record GetOrdersQuery() : IRequest<List<OrderDto>>;

public class GetOrdersHandler : IRequestHandler<GetOrdersQuery, List<OrderDto>>
{
    private readonly AppDbContext _db;
    public GetOrdersHandler(AppDbContext db) => _db = db;

    public async Task<List<OrderDto>> Handle(GetOrdersQuery request, CancellationToken ct)
    {
        return await _db.Orders
            .Select(o => new OrderDto(o.Id, o.ProductName, o.Quantity))
            .ToListAsync(ct);
    }
}
  

🔹 5. Advantages

  • Scalability (reads/writes scale independently)

  • Optimized models (read side can be tailored for UI needs)

  • Fits well with Event Sourcing & microservices

  • Better separation of concerns

🔹 6. Drawbacks

  • Complexity → More moving parts (commands, queries, handlers)

  • Eventual Consistency → Read model may lag behind write model in distributed systems

  • Infrastructure Overhead → Might require message bus, projections, or multiple DBs

  • Not needed for simple CRUD apps

🔹 7. When to Use

✅ Best for:

  • Large enterprise systems

  • Read-heavy applications (e.g., dashboards, e-commerce product catalogs)

  • Event-driven microservices

  • Systems needing audit trails & history

❌ Avoid for:

  • Simple CRUD apps

  • Small internal tools or prototypes

⚖️ CQRS Pros vs Cons

AspectPros ✅Cons ⚠️
ScalabilityReads and writes can scale independentlyMore moving parts → harder to manage infra
PerformanceQueries can use denormalized models, caching, or NoSQL for speedPossible eventual consistency (read may lag behind write)
Separation of ConcernsClear distinction: Commands = state changes, Queries = reads onlyExtra boilerplate (commands, queries, DTOs, handlers)
FlexibilityCan use different databases for read/write sidesRequires integration between multiple stores
Event SourcingNatural fit, allows audit logs, replay, and history trackingAdds complexity in storage & replay logic
MaintainabilityCleaner domain model, easier to evolve business rulesSteeper learning curve for new teams
User ExperienceRead side tailored for UI → faster responsesDebugging across async events + projections is harder
Best ForLarge-scale, complex, event-driven apps (e.g., e-commerce, banking)Not ideal for simple CRUD apps or MVPs