The Mediator pattern, when applied to CQRS, doesn’t actually make your handlers more decoupled from each other; it makes them less coupled to the specific orchestrator that calls them.
Let’s see this in action. Imagine a simple e-commerce system with commands for creating an order and queries for retrieving order details.
Here’s a basic CreateOrderCommand and its handler:
public class CreateOrderCommand : ICommand
{
public Guid OrderId { get; set; }
public List<OrderItem> Items { get; set; }
public string CustomerId { get; set; }
}
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
private readonly IOrderRepository _orderRepository;
private readonly IEventPublisher _eventPublisher;
public CreateOrderCommandHandler(IOrderRepository orderRepository, IEventPublisher eventPublisher)
{
_orderRepository = orderRepository;
_eventPublisher = eventPublisher;
}
public async Task Handle(CreateOrderCommand command)
{
var order = new Order(command.OrderId, command.CustomerId, command.Items);
await _orderRepository.SaveAsync(order);
await _eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id, order.CustomerId));
}
}
And a GetOrderByIdQuery and its handler:
public class GetOrderByIdQuery : IQuery<OrderDto>
{
public Guid OrderId { get; set; }
}
public class GetOrderByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
private readonly IOrderRepository _orderRepository;
public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<OrderDto> Handle(GetOrderByIdQuery query)
{
var order = await _orderRepository.GetByIdAsync(query.OrderId);
return new OrderDto { OrderId = order.Id, CustomerId = order.CustomerId, TotalAmount = order.CalculateTotal() };
}
}
Before the Mediator, you’d likely have a controller or a service that directly instantiates and calls these handlers:
public class OrderService
{
private readonly IServiceProvider _serviceProvider; // Or direct dependencies
public OrderService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task PlaceOrder(CreateOrderCommand command)
{
var handler = _serviceProvider.GetService<ICommandHandler<CreateOrderCommand>>();
await handler.Handle(command);
}
public async Task<OrderDto> GetOrder(Guid orderId)
{
var query = new GetOrderByIdQuery { OrderId = orderId };
var handler = _serviceProvider.GetService<IQueryHandler<GetOrderByIdQuery, OrderDto>>();
return await handler.Handle(query);
}
}
This works, but the OrderService is now tightly coupled to the interface of the handlers it’s calling and how to find them (e.g., via IServiceProvider). The handlers themselves are well-decoupled from each other, but the orchestrator is doing the heavy lifting of finding and invoking them.
Now, let’s introduce a Mediator. A Mediator is essentially a central point that receives a message (command or query) and dispatches it to the appropriate handler. You don’t need to know which handler will process it, only that the Mediator does.
Using a popular library like MediatR, your setup looks like this:
First, register your handlers with the dependency injection container. MediatR typically scans assemblies for IRequestHandler implementations.
// In your Startup.cs or Program.cs
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
services.AddTransient<IOrderRepository, OrderRepository>();
services.AddTransient<IEventPublisher, EventPublisher>();
Your handlers remain largely the same, but they now implement IRequestHandler<TRequest, TResponse> (or IRequestHandler<TRequest> for commands).
// Handler for CreateOrderCommand (no change needed in handler logic)
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
private readonly IOrderRepository _orderRepository;
private readonly IEventPublisher _eventPublisher;
public CreateOrderCommandHandler(IOrderRepository orderRepository, IEventPublisher eventPublisher)
{
_orderRepository = orderRepository;
_eventPublisher = eventPublisher;
}
public async Task Handle(CreateOrderCommand command, CancellationToken cancellationToken) // Note CancellationToken
{
var order = new Order(command.OrderId, command.CustomerId, command.Items);
await _orderRepository.SaveAsync(order);
await _eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id, order.CustomerId));
}
}
// Handler for GetOrderByIdQuery (no change needed in handler logic)
public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto>
{
private readonly IOrderRepository _orderRepository;
public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<OrderDto> Handle(GetOrderByIdQuery query, CancellationToken cancellationToken) // Note CancellationToken
{
var order = await _orderRepository.GetByIdAsync(query.OrderId);
return new OrderDto { OrderId = order.Id, CustomerId = order.CustomerId, TotalAmount = order.CalculateTotal() };
}
}
The key change is in the consumer of the handlers. Instead of directly resolving and calling handlers, you inject the IMediator interface:
public class OrderController : Controller
{
private readonly IMediator _mediator;
public OrderController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
await _mediator.Send(command); // Send the command
return Ok();
}
[HttpGet("orders/{orderId}")]
public async Task<ActionResult<OrderDto>> GetOrder(Guid orderId)
{
var query = new GetOrderByIdQuery { OrderId = orderId };
var orderDto = await _mediator.Send(query); // Send the query
return Ok(orderDto);
}
}
The Mediator handles finding the correct CreateOrderCommandHandler for the CreateOrderCommand and the GetOrderByIdQueryHandler for the GetOrderByIdQuery. Your OrderController (or any other service) is now only dependent on the IMediator interface, not on any specific handler or how to resolve them. The handlers remain focused solely on their business logic.
This pattern is powerful because it centralizes the dispatching logic. You can add cross-cutting concerns like logging, validation, or authorization as behaviors or pipeline steps that wrap the handler execution. For example, a validation behavior could intercept the Send call before it reaches the CreateOrderCommandHandler, check the command’s validity, and throw an exception if it’s invalid, all without the handler needing to know about validation.
The true power of the Mediator pattern in CQRS isn’t that it decouples handlers from each other (they were already decoupled by their interfaces). It’s that it decouples consumers of commands and queries from the specific implementation details of how those commands and queries are handled. You push the responsibility of "who handles this?" from your application services and controllers into the Mediator infrastructure, allowing those components to focus purely on their own concerns (e.g., API endpoints, UI logic).
The next step in mastering CQRS is often exploring the concept of event sourcing, where your state changes are stored as a sequence of immutable events rather than just the current state.