Request Response Messaging Pattern With MassTransit

Milan Jovanović - Apr 30 - - Dev Community

Building distributed applications might seem simple at first. It's just servers talking to each other. Right?

However, it opens a set of potential problems you must consider. What if the network has a hiccup? A service unexpectedly crashes? You try to scale, and everything crumbles under the load? This is where the way your distributed system communicates becomes critical.

Traditional synchronous communication, where services call each other directly, is inherently fragile. It creates tight coupling, making your whole application vulnerable to single points of failure.

To combat this, we can turn to distributed messaging (and introduce an entirely different set of problems, but that's a story for another issue).

One powerful tool for achieving this in the .NET world is MassTransit.

In this week's issue, we'll explore MassTransit's implementation of the request-response pattern.

Request-Response Messaging Pattern Introduction

Let's start by explaining how the request-response messaging pattern works.

The request-response pattern is just like making a traditional function call but over the network. One service, the requester, sends a request message and waits for a corresponding response message. This is a synchronous communication approach from the requester's side.

The good parts:

  • Loose Coupling: Services don't need direct knowledge of each other, only of the message contracts. This makes changes and scaling easier.
  • Location Transparency: The requester doesn't need to know where the responder is located, leading to improved flexibility.

The bad parts:

  • Latency: The overhead of messaging adds some additional latency.
  • Complexity: Introducing a messaging system and managing the additional infrastructure can increase project complexity.

Image description

Request-Response Messaging With MassTransit

MassTransitsupports the request-response messaging pattern out of the box. We can use a request client to send requests and wait for a response. The request client is asynchronous and supports the await keyword. The request will also have a timeout of 30 seconds by default, to prevent waiting for the response for too long.

Let's imagine a scenario where you have an order processing system that needs to fetch an order's latest status. We can fetch the status from an Order Management service. With MassTransit, you'll create a request client to initiate the process. This client will send a GetOrderStatusRequest message onto the bus.

public record GetOrderStatusRequest
{
    public string OrderId { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

On the Order Management side, a responder (or consumer) will be listening for GetOrderStatusRequest messages. It receives the request, potentially queries a database to get the status, and then sends a GetOrderStatusResponse message back onto the bus. The original request client will be waiting for this response and can then process it accordingly.

public class GetOrderStatusRequestConsumer : IConsumer<GetOrderStatusRequest>
{
    public async Task Consume(ConsumeContext<GetOrderStatusRequest> context)
    {
        // Get the order status from a database.

        await context.ResponseAsync<GetOrderStatusResponse>(new
        {
            // Set the respective response properties.
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Getting User Permissions In a Modular Monolith

Here's a real-world scenario where my team decided to implement this pattern. We were building a modular monolith, and one of the modules was responsible for managing user permissions. The other modules could call out to the Users module to get the user's permissions. And this works great while we are still inside a monolith system.

However, at one point we needed to extract one module into a separate service. This meant that the communication with the Users module using simple method calls would no longer work.

Luckily, we were already using MassTransit and RabbitMQ for messaging inside the system.

So, we decided to use the MassTransit request-response feature to implement this.

The new service will inject an IRequestClient<GetUserPermissions>. We can use it to send a GetUserPermissions message and await a response.

A very powerful feature of MassTransit is that you can await more than one response message. In this example, we're waiting for a PermissionsResponse or an Error response. This is great, because we also have a way to handle failures in the consumer.

internal sealed class PermissionService(
    IRequestClient<GetUserPermissions> client)
    : IPermissionService
{
    public async Task<Result<PermissionsResponse>> GetUserPermissionsAsync(
        string identityId)
    {
        var request = new GetUserPermissions(identityId);

        Response<PermissionsResponse, Error> response =
            await client.GetResponse<PermissionsResponse, Error>(request);

        if (response.Is(out Response<Error> errorResponse))
        {
            return Result.Failure<PermissionsResponse>(errorResponse.Message);
        }

        if (response.Is(out Response<PermissionsResponse> permissionResponse))
        {
            return permissionResponse.Message;
        }

        return Result.Failure<PermissionsResponse>(NotFound);
    }
}
Enter fullscreen mode Exit fullscreen mode

In the Users module, we can easily implement the GetUserPermissionsConsumer. It will respond with a PermissionsResponse if the permissions are found or an Error in case of a failure.

public sealed class GetUserPermissionsConsumer(
    IPermissionService permissionService)
    : IConsumer<GetUserPermissions>
{
    public async Task Consume(ConsumeContext<GetUserPermissions> context)
    {
        Result<PermissionsResponse> result =
            await permissionService.GetUserPermissionsAsync(
                context.Message.IdentityId);

        if (result.IsSuccess)
        {
            await context.RespondAsync(result.Value);
        }
        else
        {
            await context.RespondAsync(result.Error);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Closing Thoughts

By embracing messaging patterns with MassTransit, you're building on a much sturdier foundation. Your .NET services are now less tightly coupled, giving you the flexibility to evolve them independently and weather those inevitable network glitches or service outages.

The request-response pattern is a powerful tool in your messaging arsenal. MassTransit makes it remarkably easy to implement, ensuring that requests and responses are delivered reliably.

We can use request-response to implement communication between modules in a modular monolith. However, don't take this to the extreme, or your system might suffer from increased latency.

Start small, experiment, and see how the reliability and flexibility of messaging can transform your development experience.

That's all for this week. Stay awesome!


P.S. Whenever you're ready, there are 3 ways I can help you:

  1. Modular Monolith Architecture (NEW): Join 450+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.

  2. Pragmatic Clean Architecture: Join 2,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.

  3. Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .