NashTech Blog

Exploring CQRS Through CQS and the Mediator Pattern

Table of Contents

When you first start building an application, things are usually simple. But as you add more features and more business rules, your code can become a messy web of connections. This is a common problem that developers call “spaghetti code,” because it’s tangled and hard to follow.

So, how can we keep our applications clean and organized as they grow? One of the best answers is an architectural pattern called CQRS (Command Query Responsibility Segregation).

In this complete guide, we will explore CQRS in detail. We will learn:

  • The foundational rule it is built on: CQS.
  • The powerful helper pattern that makes it work: the Mediator Pattern.
  • From CQS to Mediator to CQRS
  • Advantages and Disadvantages of CQRS

What is CQS?

Before we can understand CQRS, we must start with a basic rule called CQS. It was created by a computer scientist named Bertrand Meyer. The main idea of CQS is:

A function should either do something (a Command) or answer something (a Query) – never both.

Here is a method that breaks the CQS rule:

public interface IOrderCommandService
{
    void PlaceOrder(Order order);
    void CancelOrder(long orderId);
}

public interface IOrderQueryService
{
    Order GetById(long orderId);
    IEnumerable<Order> GetOrdersByCustomer(long customerId);
}

This design is much safer, more predictable, and easier to test.

While CQS offers significant benefits, it’s important to acknowledge its trade-offs. The most notable is a potential increase in verbosity. Separating every action into distinct command or query classes naturally leads to more files and boilerplate code. For very small or simple applications, this structured approach might feel like unnecessary overhead.

However, a more critical challenge arises not within the services themselves, but in how they are consumed. A common implementation involves injecting these granular services directly into a controller. While an IOrderCommandService on its own is perfectly fine, the problem appears when a controller needs to coordinate multiple operations:

public class DashboardController : Controller
{
    private readonly IOrderCommandService _orderCommands;
    private readonly IProductQueryService _productQueries;
    private readonly IUserQueryService _userQueries;
    
    // This constructor is becoming bloated!
    public DashboardController(
        IOrderCommandService orderCommands,
        IProductQueryService productQueries,
        IUserQueryService userQueries)
    {
        // ...
    }
}

This leads to “Controller Bloat,” a clear violation of the Single Responsibility Principle (SRP). The controller’s primary job is to handle HTTP flow, not to be a container for numerous business logic services. This tight coupling makes the controller difficult to test and maintain. This very problem is why the CQS pattern is almost always paired with the Mediator pattern to keep controllers lean and clean.

The Mediator Pattern: The Missing Piece

The Mediator pattern solves the controller bloat problem by introducing an intermediary that handles communication between objects. Instead of your controller knowing about every single service, it only needs to know about one thing: the mediator.

Think of it like a switchboard operator in old telephone systems. Instead of each phone having direct wires to every other phone (impossible to manage), all calls go through the operator who routes them correctly.

In CQRS terms, the mediator receives commands and queries, then routes them to the appropriate handlers. Your controller becomes blissfully simple:

public class DashboardController : Controller
{
    private readonly IMediator _mediator;
    
    //Only one dependency.
    public DashboardController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    public async Task<IActionResult> GetUserOrders(long userId)
    {
        var query = new GetOrdersByCustomerQuery(userId);
        var orders = await _mediator.Send(query);
        return Ok(orders);
    }
}

Now the controller has:

  • Now the controller has:
  • Only one dependency
  • Only one responsibility
  • Clean, easy-to-read code

From CQS to CQRS

CQS is about methods:

A method should either change data (Command) or return data (Query).

But when an application becomes larger, separating just the methods is not enough. We need a clearer structure across the whole system. This is where CQRS comes in.

With Mediator, we naturally evolve from CQS to CQRS (Command Query Responsibility Segregation).

  • CQS separates the methods (interfaces).
  • CQRS separates the models. You have distinct classes for CreateOrderCommand (Write model) and GetOrderByIdQuery (Read model).
// COMMANDS 
public sealed record CreateOrderCommand(
    long CustomerId,
    List<OrderLineItem> Items,
    string ShippingAddress
) : ICommand<CreateOrderResult>;

public sealed record CreateOrderResult(
    long OrderId,
    bool Success,
    string? ErrorMessage = null
);

public sealed record UpdateOrderStatusCommand(
    long OrderId,
    OrderStatus NewStatus
) : ICommand<bool>;

// QUERIES 
public sealed record GetOrderByIdQuery(
    long OrderId
) : IQuery<OrderDto?>;

public sealed record GetOrdersByCustomerQuery(
    long CustomerId
) : IQuery<List<OrderSummaryDto>>;

// DTOs optimized for reading
public sealed record OrderDto(
    long Id,
    long CustomerId,
    string CustomerName,
    DateTime OrderDate,
    OrderStatus Status,
    decimal Total
);

public sealed record OrderSummaryDto(
    long Id,
    DateTime OrderDate,
    OrderStatus Status,
    decimal Total
);

One Dedicated Handler

// Each handler focuses on ONE thing
   public sealed class CreateOrderHandler 
       : ICommandHandler<CreateOrderCommand, CreateOrderResult>
   {
       // Only contains logic for creating orders
       // No query logic mixed in
   }
   
   public sealed class GetOrderByIdHandler 
       : IQueryHandler<GetOrderByIdQuery, OrderDto?>
   {
       // Only contains logic for reading one order
       // No command logic mixed in
   }

For example, we implement CreateOrderHandler

public sealed class CreateOrderHandler 
    : ICommandHandler<CreateOrderCommand, CreateOrderResult>
{
    private readonly IWriteDbContext _writeDb;
    
    public async ValueTask<CreateOrderResult> Handle(
        CreateOrderCommand command, 
        CancellationToken cancellationToken)
    {
        // your code...
        var order = new Order { /* ... */ };
        _writeDb.Orders.Add(order);
        await _writeDb.SaveChangesAsync(cancellationToken);
        return new CreateOrderResult(order.Id, true);
    }
}

As the Unix philosophy states: “Do one thing, and do it well.”

// BEFORE (Traditional Service)
OrderService
├── CreateOrder()          // Does many things
├── GetOrder()             // Does many things
├── UpdateOrderStatus()    // Does many things
└── GetOrdersByCustomer()  // Does many things

// AFTER (CQRS)
CreateOrderCommand + CreateOrderHandler        // Does ONE thing well
GetOrderByIdQuery + GetOrderByIdHandler       // Does ONE thing well
UpdateOrderStatusCommand + Handler            // Does ONE thing well
GetOrdersByCustomerQuery + Handler            // Does ONE thing well

Here’s what happens when you execute a command or query:

┌─────────────┐
│  Controller │
│             │
│  Receives   │
│  HTTP       │
│  Request    │
└──────┬──────┘
       │
       │ Creates Command/Query
       │
       ▼
┌─────────────────────────────────┐
│  Command: CreateOrderCommand    │
│  {                              │
│    CustomerId: 123,             │
│    Items: [...],                │
│    ShippingAddress: "..."       │
│  }                              │
└──────┬──────────────────────────┘
       │
       │ Send to Mediator
       │
       ▼
┌─────────────────────────────────┐
│         IMediator               │
│                                 │
│  mediator.Send(command)         │
│                                 │
│  "Who handles this command?"    │
└──────┬──────────────────────────┘
       │
       │ Routes to Handler
       │
       ▼
┌─────────────────────────────────┐
│   CreateOrderHandler            │
│                                 │
│  1. Validate customer           │
│  2. ....                        │
│  n. Return result               │
└──────┬──────────────────────────┘
       │
       │ Returns Result
       │
       ▼
┌─────────────────────────────────┐
│  CreateOrderResult              │
│  {                              │
│    OrderId: 7891,               │
│    Success: true                │
│  }                              │
└──────┬──────────────────────────┘
       │
       │ Back to Controller
       │
       ▼
┌─────────────┐
│  Controller │
│             │
│  Returns    │
│  HTTP 201   │
│  Created    │
└─────────────┘

Advantages and Disadvantages of CQRS

CQRS brings some practical benefits:

  1. Cleaner structure
    Read and write logic are separated, making code easier to follow.
  2. Controllers become small and simple
    Because they only talk to the mediator.
  3. Easier to test
    Each command or query has its own handler, so testing becomes straightforward.
  4. Better flexibility for the future
    When the system gets bigger, it’s easier to evolve without breaking old logic.

Disadvantages of CQRS

It’s not perfect, of course:

  1. More files
    Commands, queries, and handlers can increase the number of classes in the project.
  2. Can feel like overkill for small apps
    If your project is simple CRUD, CQRS might be more structure than you really need.
  3. More concepts to learn
    Developers new to CQRS or Mediator may take time to get comfortable.

Closing Thoughts

CQRS isn’t a magical solution, but it’s a practical way to keep growing applications clean and organized. When combined with the Mediator pattern, it helps avoid controller bloat and brings more clarity to your architecture.

If your project is expanding and business rules are getting heavier, CQRS can be a great fit. If it’s small, you might wait until things grow before adopting it.

Picture of Tam Chieu Minh

Tam Chieu Minh

In the craft of .NET engineering, code is the blade and logic is the mind. Steady practice overcomes complexity, and patience turns chaos into order. Those who refine their tools daily endure in the long run.

Leave a Comment

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

Suggested Article

Scroll to Top