• Join Postman CTO, Ankit Sobti, and Head of Customer Experience and Success, Kristine Chin, at this webinar which delivers the information you need to maximize the success of your API products, reduce friction to collaboration, and to provide a world-class experience for your developers, partners, and customers.
Introduction
CQRS (Command Query Responsibility Segregation) is a design pattern that separates the read (query) and write (command) operations of an application, leading to better maintainability, scalability, and flexibility.
It's particularly suitable for applications with complex business logic, high read/write ratio, or a need to scale independently.
How it works:
• Commands: Operations that change the state of the system. Commands usually don't return data, only the status of the operation.
• Queries: Operations that retrieve data from the system. Queries only read data and don't modify the system state.
CQRS in Microservices and Separate Databases
When using CQRS in a microservices architecture, you can separate read and write operations within individual services or across multiple services, providing several benefits:
• Event Sourcing: Combine CQRS with Event Sourcing for better auditability, data versioning, and troubleshooting.
• Separate Data Stores: Maintain separate data stores for read and write sides, optimizing performance and ensuring data consistency.
• Independent Scaling: Scale the read and write sides of your microservices independently for optimal resource usage.Using separate databases for reading and writing can offer several advantages:
• Independent Scaling: Scale the read and write databases independently based on your application's needs.
• Flexibility: Choose the most suitable database technology for each side of your application.
This is the most common combination of implementation of the CQRS pattern seen on projects of the past few years.
Mediator Pattern: MediatR follows the mediator design pattern, where a central mediator object facilitates communication between different components without them needing to be aware of each other. This reduces the coupling between components and makes the system easier to maintain and evolve.
But is this really necessary?
Of course not.
I will show that in the following text.
Clean CQRS
The project CQRS has a following structure:
To implement the CQRS pattern, without using any libraries, it is necessary to create only 4 interfaces.
All 4 interfaces are located in the Common folder:
- IQueryHandler - This interface is responsible for handling query operations. It has a single Handle method that takes a query object of type TQuery and a cancellation token, and returns a Task representing the result of the query operation.
public interface IQueryHandler<in TQuery, TQueryResult>
{
Task<TQueryResult> Handle(TQuery query, CancellationToken cancellation);
}
- ICommandHandler - This interface is responsible for handling command operations.
public interface ICommandHandler<in TCommand, TCommandResult>
{
Task<TCommandResult> Handle(TCommand command, CancellationToken cancellation);
}
- IQueryDispatcher - This interface is responsible for dispatching queries to their respective query handlers. It has a generic Dispatch method that takes a query object and a cancellation token, and returns a Task representing the result of the dispatched query.
public interface IQueryDispatcher
{
Task<TQueryResult> Dispatch<TQuery, TQueryResult>(TQuery query, CancellationToken cancellation);
}
- ICommandDispatcher - This interface is responsible for dispatching commands to their respective command handlers.
public interface ICommandDispatcher
{
Task<TCommandResult> Dispatch<TCommand, TCommandResult>(TCommand command, CancellationToken cancellation);
}
In order for dispatchers to know which handlers (queries or commands) they will call, it is necessary to tell them how to select handlers.
That is why we will make implementations of both dispatchers. Place them in the Dispatchers folder.
QueryDispatcher implementation:
public class QueryDispatcher(IServiceProvider serviceProvider) : IQueryDispatcher
{
private readonly IServiceProvider _serviceProvider = serviceProvider;
public Task<TQueryResult> Dispatch<TQuery, TQueryResult>(TQuery query, CancellationToken cancellation)
{
var handler = _serviceProvider.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>();
return handler.Handle(query, cancellation);
}
}
CommandDispatcher implementation:
public class CommandDispatcher(IServiceProvider serviceProvider) : ICommandDispatcher
{
private readonly IServiceProvider _serviceProvider = serviceProvider;
public Task<TCommandResult> Dispatch<TCommand, TCommandResult>(TCommand command, CancellationToken cancellation)
{
var handler = _serviceProvider.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>();
return handler.Handle(command, cancellation);
}
}
The Dispatch method of the CommandDispatcher class takes two type parameters, TCommand and TCommandResult, and two arguments, command and cancellation, respectively.
Within the Dispatch method, the appropriate ICommandHandler<TCommand, TCommandResult> is obtained from the _serviceProvider field using the GetRequiredService method, which returns a new instance of the service.
Then the Handle method of the obtained handler is called with the provided command and cancellation arguments. Finally, the Task returned from the Handle method is returned from the Dispatch method.
How to use it?
Let's say we have a UsersController that represents an endpoint in the API and returns a user with a given Id. Since it queries the database, we know that we will have some Query and QueryHandler.
[Route("api/[controller]")]
[ApiController]
public class UsersController(IQueryDispatcher queryDispatcher, ICommandDispatcher commandDispatcher) : ControllerBase
{
private readonly IQueryDispatcher _queryDispatcher = queryDispatcher;
private readonly ICommandDispatcher _commandDispatcher = commandDispatcher;
[HttpGet("{id}")]
public async Task<IActionResult> GetById(Guid id, CancellationToken cancellationToken)
{
var query = new GetUserByIdQuery { UserId = id };
var user = await _queryDispatcher.Dispatch<GetUserByIdQuery, User>(query, cancellationToken);
if (user == null)
{
return NotFound();
}
return Ok(user);
}
}
The _queryDispatcher.Dispatch method sends the query to the appropriate query handler registered with the application's dependency injection container, which executes the query and returns the result.
In this case, the query handler retrieves the user with the specified ID from the data store and returns it as a User object as result.
In order for this to work, it is necessary to create a Query and its Handler.
GetUserByIdQuery is nothing but a simple wrapper around Id.
For the same Query, there is also a QueryHandler that will be called when the Dispatcher dispatches this Query.
The QueryHandler looks like this:
public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, User>
{
public GetUserByIdQueryHandler() { }
public async Task<User> Handle(GetUserByIdQuery query, CancellationToken cancellationToken)
{
//Call Repository
return new User();
}
}
For every other command or query, you would create the same class structure.
Wrapping Up
That's all from me today.
Using MediatR in CQRS implementation is not wrong. Considering the increasing use of 'Clean Architecture', it is a great way to make code more readable and maintainable.
It's Monday, make a coffee and check the whole project implementation on GitHub repository.