CQRS Pattern Explained: Separating Reads from Writes

CQRS—Command Query Responsibility Segregation—sounds intimidating, but the core idea is simple. Separate your read operations from your write operations. Here’s why you’d want to do that and how to get started.

The Problem

In traditional CRUD applications, the same model handles both reads and writes. This works fine until:

  • Your read queries need different data shapes than your write models
  • Reads vastly outnumber writes and have different performance needs
  • You need to scale reads and writes independently
  • Your domain model is complex but your read views are simple

The CQRS Solution

Split your system into two sides:

  • Commands: Operations that change state (Create, Update, Delete)
  • Queries: Operations that return data without side effects (Read)
// Command side - rich domain model
public class PlaceOrderCommand
{
    public Guid CustomerId { get; set; }
    public List Lines { get; set; }
}

public class PlaceOrderHandler
{
    public async Task Handle(PlaceOrderCommand command)
    {
        var customer = await _customerRepo.GetById(command.CustomerId);
        var order = customer.PlaceOrder(command.Lines);
        await _orderRepo.Save(order);
    }
}

// Query side - simple DTOs optimized for reads
public class OrderSummaryQuery
{
    public Guid OrderId { get; set; }
}

public class OrderSummaryHandler
{
    public async Task Handle(OrderSummaryQuery query)
    {
        // Direct database query, no domain model
        return await _db.QueryFirstAsync(
            "SELECT Id, Total, Status FROM Orders WHERE Id = @Id",
            new { query.OrderId });
    }
}

When to Use CQRS

CQRS adds complexity. Don’t use it for simple CRUD apps. Consider it when:

  • Read and write workloads are significantly different
  • You need to scale reads and writes independently
  • Your domain is complex but read views are simple
  • You’re using Event Sourcing (they pair well together)

Implementation with MediatR

MediatR is a popular library that makes CQRS easy in .NET:

// Command
public class CreateUserCommand : IRequest
{
    public string Email { get; set; }
    public string Name { get; set; }
}

// Handler
public class CreateUserHandler : IRequestHandler
{
    public async Task Handle(CreateUserCommand request, CancellationToken ct)
    {
        var user = new User(request.Email, request.Name);
        await _repository.Add(user);
        return user.Id;
    }
}

// Controller
[HttpPost]
public async Task> Create(CreateUserCommand command)
{
    var id = await _mediator.Send(command);
    return Ok(id);
}

References


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.