CQRS pattern

Introduction

When building enterprise applications, we often start with a simple CRUD approach. It works fine until it faces some issues and system grows more we may encounter several challenges.

As systems grow, business rules become complex, read traffic increases, and scalability becomes critical the traditional “one model for everything” approach starts to show cracks.

That’s where CQRS (Command Query Responsibility Segregation) comes in and resolves these issues.

In this article, I’ll walk you through:

What is CQRS (Command Query Responsibility Segregation)?

CQRS is a design pattern that separates read operations (queries) from write operations (commands) in an application.

Instead of using the same model for both reading and writing data, CQRS splits them into two different models:

This improves scalability, maintainability, and clarity in complex systems.

CQRS separates read operations from write operations. We never mix command and queries.

Basic Idea

Traditional CRUD approach:

Controller → Service → Repository → Database

CQRS approach:

Command → Command Handler → Domain → Database
Query   → Query Handler   → Read Model → Database

Visual Understanding

CQRS

Why Use CQRS?

Let’s think practically. In most applications:

Trying to optimize both using the same model leads to complexity.

CQRS allows:

If you’re already using Clean Architecture CQRS fits naturally.

1. Clear Separation of Concerns

Reads and writes have different responsibilities.

2. Scalability

3. Performance Optimization

4. Works Great with:

Where MediatR Fits In

MediatR is a lightweight mediator library that helps implement CQRS cleanly.

Instead of controllers directly calling services:

Controller → Service → Repository

We do:

Controller → MediatR → Handler

Each request (command/query) has exactly one handler.

This removes tight coupling and improves maintainability.

Project Structure (.NET 10 Web API)

A clean CQRS structure typically looks like:

Application
 ├── Commands
 ├── Queries
 ├── Handlers
Domain
Infrastructure
API (Controllers)

This aligns perfectly with Clean Architecture principles.

Step-by-Step: CQRS with MediatR in .NET 10

Create a new WebAPI project in .NET 10 or you can you your existing project and implement CQRS with MediatR in .NET 10

Then you will follow the below.

Install Required Packages

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Register MediatR in Program.cs

builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

That’s it. Clean and simple.

Implementing Command Side (Write Model)

Example: Create Booking

Add a new record CreateBookingCommand

public record CreateBookingCommand(
    string GuestName,
    DateTime CheckIn
) : IRequest<Guid>;

Note:

Now, let’s create a new CreateBookingCommandHandler class

public class CreateBookingCommandHandler 
    : IRequestHandler<CreateBookingCommand, Guid>
{
    private readonly AppDbContext _context;

    public CreateBookingCommandHandler(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Guid> Handle(
        CreateBookingCommand request, 
        CancellationToken cancellationToken)
    {
        var booking = new Booking
        {
            GuestName = request.GuestName,
            CheckIn = request.CheckIn
        };

        _context.Bookings.Add(booking);
        await _context.SaveChangesAsync(cancellationToken);

        return booking.Id;
    }
}

Implementing Query Side (Read Model)

Now we separate the read logic.

Let’s create a record class for GetBooking

public record GetBookingByIdQuery(Guid Id) : IRequest<BookingDto>;<br>

Note:

Now, let’s create GetBookingByIdQueryHandler class

public class GetBookingByIdQueryHandler
    : IRequestHandler<GetBookingByIdQuery, BookingDto>
{
    private readonly AppDbContext _context;

    public GetBookingByIdQueryHandler(AppDbContext context)
    {
        _context = context;
    }

    public async Task<BookingDto> Handle(
        GetBookingByIdQuery request,
        CancellationToken cancellationToken)
    {
        return await _context.Bookings
            .AsNoTracking()
            .Where(x => x.Id == request.Id)
            .Select(x => new BookingDto
            {
                Id = x.Id,
                GuestName = x.GuestName,
                CheckIn = x.CheckIn
            })
            .FirstOrDefaultAsync(cancellationToken);
    }
}

Important:

Controller Usage

Controllers become thin.

[ApiController]
[Route("api/bookings")]
public class BookingsController : ControllerBase
{
    private readonly IMediator _mediator;

    public BookingsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateBookingCommand command)
    {
        var id = await _mediator.Send(command);
        return Ok(id);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(Guid id)
    {
        var result = await _mediator.Send(
            new GetBookingByIdQuery(id));

        if (result == null)
            return NotFound();

        return Ok(result);
    }
}

Now the controller:

This is clean architecture in action.

Improvements (Production Ready)

In real systems, you usually add:

Use FluentValidation with MediatR pipeline.

Centralized logging without polluting handlers.

Wrap command handlers in transactions.

In high-scale systems:

When Should You Use CQRS?

Use CQRS pattern when:

Avoid it when:

CQRS adds structure — and structure adds complexity.

Use it wisely.

Key Takeaways

Conclusion

CQRS helps you build clearer, more maintainable systems by separating reads from writes. When combined with MediatR in .NET 10, it promotes clean architecture, focused handlers, and thin controllers making your code easier to test, scale, and evolve.

While it may be unnecessary for small CRUD apps, CQRS becomes highly valuable in complex or growing systems. When it is used wisely, it provides a solid foundation for building modern, scalable enterprise applications.