...

Simplifying System Design With Mediator Pattern

For a couple of years now we see a rightful trend of simplifying applications and making user interactions with even very complex systems as smooth and as sleak as possible. However, to achieve such a simplicity and comfort in the user experience area, it’s not an unusual case that underneath the hood systems run quite complex processing algorithms and operate with numerous different APIs. In this article I would like to share some ideas on how, with just a couple of development tricks you may also simplify your back-end code, making it more concise, easier to maintain and potentially making it more fun to work with.

Architecture

A simplified example of a complex system could look similar to the one presented in the diagram below. There’s a device/robot, which is capable of sending telemetry data with information about its health, working conditions or the surroundings. On the other side we have a user application, providing all sorts valuable information, potentially helping a device operator in managing it.

Diagram focuses solely on a single use case of such a system – requesting an information about a selected device type.

From device perspective we have the following flow:

  • Robot sends a telemetry message
  • Telemetry Data Processor is responsible for validating this message. In order to do so, it has to check within Device Types Registry, if a corresponding type exists.

From an application perspective we have the following flow:

  • application wants to display selected device type details
  • application issues a request to Device Type Registry for these details

Challenge

As it usually happens in such systems, we have a shared functionality, accessible for different types of receivers through most probably different means of transportation. Additionally, just as in the diagram above, in many cases an access to particular functionality might also have different security requirements. In the end however, a result of a call to both Internal API and External API ends up in the same data store, operates within the same device types domain, uses the same models and filtering capabilities.

Service setup

An approach to organizing communication in such a system could be the following:

  • for an Internal API use GRPC based communication. GRPC protocol becomes increasingly popular. Its efficiency and small footprint makes it a great choice for such a service-to-service type of communication.
  • for External API use widely adopted REST based communication.

Obviously, such an approach requires separate infrastructure and communication stack. At the same time however, the goal should be to make these infrastructure layers as thin as possible, do not leak any of the business logic parts into these communication bits, still keeping the code free of duplicates, modular and testable.

One of the possible and quite neat solutions here would be a use of mediator pattern.
Mediator pattern is one of behavioral patterns. In its essence, it allows developers to define how different objects interact with each other, without a requirement for these objects to reference each other directly. Such an approach allows for a more loosely coupled architecture, independent development of individual classes and quite handy components reusability.

The key aspects of an approach suggested in the above diagram are:

  1. Usage of API specific infrastructure layer. This might be your ASP.NET Core Web API setup with some middleware and a couple of controller classes. This might also be a GRPC “.proto” file and an automatically generated service classes.
  2. Optional usage of API specific Response Mapping component. This might be helpful, when there’s a need to adjust some protocol specific response parts (ie. http response status codes). Such a layer in a very simplified manner may also apply for response content mapping, when there’s a requirement for the particular API not to expose all parts of model, which may be a result of Request Handler execution.
  3. Usage of Mediator and Request Handler. The handler here is actually the key part to the presented approach. This is where the whole business logic is kept. It is infrastructure or framework independent, reusable, testable and easy to maintain.

The code

In order to introduce presented approach in the code, there’s really just a handful of components required.

A Web API controller class:

public class DeviceTypesController : ControllerBase
{
    private readonly IMediator mediator;    public DeviceTypesController(IMediator mediator)
    {
        this.mediator = mediator;
    }        public Task<IActionResult> Get(
        [FromQuery] GetDeviceTypeRequest request)
    {
        return this.mediator.Send(request);
    }
}

GRPC service class:

public class DeviceTypesService 
    : DeviceTypesRegistry.DeviceTypesRegistryBase
{
    private readonly IMediator mediator;    public DeviceTypesService(IMediator mediator)
    {
        this.mediator = mediator;
    }        public override async Task<GetDeviceTypeResponse> Get(
        GetDeviceTypeRequest request, 
        ServerCallContext context)
    {
        return this.mediator.Send(request);
    }
}

The request handler:

public class GetDeviceTypeHandler 
    : IRequestHandler<GetDeviceTypeRequest, GetDeviceTypeResponse>
{
    private readonly IQueryBuilder queryBuilder;        private readonly IStorage storage;        public GetDeviceTypeHandler(
        IQueryBuilder queryBuilder, 
        IStorage storage)
    {
        this.queryBuilder = queryBuilder;
        this.storage = storage;
    }        public Task<GetDeviceTypeResponse> Handle(
        GetDeviceTypeRequest request, 
        CancellationToken cancellationToken)
    {
        //// request validation//// storage access//// etc.
    }
}

Couple of things worth noticing here:
  • The above examples are written in C#. They use tooling and packages, which are available in .NET eco-system. However, with the wealth of technologies and tools today, I’m sure that a similar approach would be as easy to implement using a completely different tech stack.
  • An important part of this implementation is the use of components available within a fantastic Jimmy Bogard’s MediatR library. It is an implementation of aforementioned mediator pattern and an essential part of the whole solution, which glues all of the bits and pieces toghether.
  • The controller class and the GRPC service class have a single dependency on IMediator interface and that part should not really change once new requirements appear. An implementation of, let’s say a device type update functionality would require defining appropriate request/response models, a handler and one more method similar to an existing Get within the controller and/or GRPC service.
  • The actual procedure of getting the data is encapsulated within the handler, which has all of the necessary dependencies. In addition, if that particular procedure changes over time, we only ever need to make changes to this single class.

Summary

This kind of approach to code organization may reward developers with number of benefits. On the number of large scale projects, built on top of different microservice orchestrators with all sorts of different APIs, using an approach similar to the presented one allowed me and my teammates not to drown in the flood of interfaces, tons of public and private methods and constantly updated method signatures. With a fantastic support of the tech stack we used, most of the times we could really focus on the actual business requirements.

One may obviously say, that instead of fighting with an overwhelming number of service specific interfaces, we ended up with myriad of small, single purpose classes. That is indeed truth and a solution here is naturally proper components naming. This however, as some may know is one of the two hardest things in programming and probably a topic for a completely different story.


Adam Zięty, Lead Software Engineer at TTMS


Concluding an article on the Mediator Pattern in C# and its importance in enhancing system architectures, it’s clear the significance of partnering with a dependable tech ally to actualize these sophisticated ideas. The team at TTMS excels in deploying Microsoft Azure solutions, crucial for tasks like Azure Cloud Migration, managing IoT data, developing in AI and Machine Learning, modernizing applications, working with Azure Kubernetes, and crafting tailor-made applications. Our expertise and proficiency enable us to convert these high-level concepts into practical, scalable, and secure solutions that propel your business ahead.

Should you need assistance in maximizing the use of Azure Cloud for refining your applications and systems, our squad is poised to tackle this challenge. We encourage you to reach out to learn how we can assist your organization in fully utilizing Microsoft Azure’s capabilities. Explore our services on the TTMS Azure page.