# Request-Response Messaging Pattern With MassTransit

> When building distributed systems with .NET, direct calls between services can create tight coupling. The request-response messaging pattern can allow distributed services to communicate in a loosely coupled way.

Published: 2024-04-27. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/request-response-messaging-pattern-with-masstransit

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**](https://milanjovanovic.tech/blog/modular-monolith-communication-patterns) 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.

![Request response messaging pattern diagram.](https://milanjovanovic.tech/blogs/mnw_087/request_response.png)

## Request-Response Messaging With MassTransit

[**MassTransit**](https://milanjovanovic.tech/blog/using-masstransit-with-rabbitmq-and-azure-service-bus)
supports the [request-response](https://masstransit.io/documentation/concepts/requests) 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.

```csharp
public record GetOrderStatusRequest
{
    public string OrderId { get; init; }
}
```

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.

```csharp
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.
        });
    }
}
```

## 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**](https://milanjovanovic.tech/modular-monolith-architecture),
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.

```csharp {11,13,18}
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);
    }
}
```

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.

```csharp
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);
        }
    }
}
```

## 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](https://youtu.be/NjsoykEOkrk) 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**](https://milanjovanovic.tech/modular-monolith-architecture).
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!

---

## Frequently asked questions

### What is the request-response messaging pattern?

It works like a traditional function call, but over a message bus. One service, the requester, sends a request message and waits for a corresponding response message. From the requester side it is synchronous, yet the services stay loosely coupled because they only share message contracts.

### How does MassTransit implement request-response?

MassTransit supports request-response out of the box through a request client. You inject an IRequestClient for the request type, send the request, and await the response. The consumer on the other side handles the message and replies with RespondAsync. Requests time out after 30 seconds by default.

### What are the tradeoffs of request-response messaging?

You get loose coupling, since services only depend on message contracts, and location transparency, since the requester does not need to know where the responder runs. In exchange, the messaging overhead adds latency, and the messaging infrastructure adds complexity to the project.

### Can a MassTransit request client wait for more than one response type?

Yes. A request client can await multiple possible response types, for example a PermissionsResponse on success or an Error response on failure. This gives you a clean way to handle failures that happen in the consumer.

### When is request-response messaging useful in a modular monolith?

When you extract a module into a separate service, in-process method calls between modules stop working. If you already run MassTransit with RabbitMQ, the request-response pattern can replace those calls. Use it sparingly, though, because overusing it increases latency.
