3

Does the mediator pattern add any value beyond dependency injection? I am encountering the mediator pattern for the first time in context of this sample application, which is meant to demonstrate how to follow Clean Architecture in .NET. I assume this pattern was included so it wouldn't be too distracting for devs who expect it to be used; but it wasn't the main focus of the sample code, or the book that this code accompanies.

In this application, we find an OrderController, which uses the mediator pattern (or at least, the MediatR library) on line 26 to retrieve data and send it back to the client. So far so good.

At this point, I'm expecting to see the receiver implemented in one of the other architectural layers. However, the receiver for this request is actually defined in the same architectural layer (the "Web" layer), leading me to question whether the mediator pattern adds any value beyond dependency injection.

Additionally, the receiver (GetMyOrdersHandler) uses bog-standard DI to access the underlying repository, which seems to put the supposedly tighter coupling in the wrong place (i.e. across the architectural layer boundary), with the loose coupling within the same layer (the UI layer).

(Side note: I have inferred from other areas in this application that the general intent is for the number of boundaries to increase as the application grows over time; however, that knowledge does not tell me why DI alone should not be used between a controller and its quintessential data.)

Furthermore, Wikipedia's description of the Mediator pattern (especially, Problems that the mediator design pattern can solve) does not clearly distinguish a use case which couldn't also be solved with DI. If two components agree to a suitable DI contract, then both of them understand exactly as much about the other as is necessary for them to communicate. What's left to abstract?

Here are the motivations for using it which I've seen so far. Many of them share a weakness in that they don't clearly explain what they mean by "interaction" or "coupling" or "peer/colleague."

  1. It's an event loop

    Although I haven't seen this mentioned explicitly, the mediator is obviously useful for processing events --- when that's exactly what you need. No-one seems to mention that, though. Also, this is obviously not the only (or perhaps even an intended) use case. Otherwise, we'd call it an event loop, and talk about algorithms instead of design patterns.

  2. It simplifies adherence to the CQRS pattern.

    Fine. But if that's the only use case, then it's not worth implementing on its own; that makes it, at most, a sub-pattern.

  3. It allows loose coupling between "peers."

    DI makes the same promise. How is this different?

  4. It avoids DI explosion by reducing the number of dependencies that need to be injected.

    This can be accomplished in other ways, like an Aggregate/Facade Service, or by breaking apart the class with the exploding constructor into mutiple classes with fewer responsibilities.

  5. It decouples the interaction of objects from their definition.

    This is hard to wrap my head around without a concrete example. What is an interaction, and why would I need to separate it out from a DI contract? How is this different from, say a service, which represents a set of interactions between a domain model and a view model --- or between the UI layer and a repository, if you like.

  6. It allows the receiver and the sender to be compiled and run separately.

    DI already provides that. All you have to do is put the contracts into a shared module which is accessible to both the sender and receiver. And, if the sender and receiver use different, incompatible programming languages, the mediator pattern doesn't save you from duplicating the contracts between them.

Sources:

2 Answers 2

3

I think your usage of "DI" is incorrect here. So I'm going to replace "DI" with "plain interface", because that is what I believe you are actually talking about.

Mediator and "plain interface" are used for the same purpse, to decouple two parts of the application, or two modules via an abstraction. For plain interfaces, this means method definitions that client would call and handler should implement. For mediator, this means classes, that represent the operation and carry data to execute that operation.

As you say, in most scenarios, the two don't have much difference, as they can achieve the same goal. But there are some differences that make each better in circumstances.

Plain interfaces are simpler, as they are natural part of the language and the ecosystem. And grouping of multiple methods into a single interface is helpful for design.

Mediator's main strength comes when you want to decorate calls between the two modules. Imagine you want to log each call between the two modules. If using interfaces, this means that each method in each interface must be implemented individually. With mediator, it is easy to just create a middleware and handle the logging for all the calls in that middleware. There are many ways this can help.

3

DI is unrelated to the mediator pattern

Wikipedia's description of the Mediator pattern (especially, Problems that the mediator design pattern can solve) does not clearly distinguish a use case which couldn't also be solved with DI

DI is a completely unrelated concept to the mediator pattern. It's leading me to wonder if your question is actually meaningfully answerable if you're going to dismiss answer that don't address your seeming definition of DI which is not lining up with what DI actually is.

The mediator pattern comes with a mediator object (which, for Mediatr is the IMediator interface). Your consumer ideally receives this mediator object as an injected dependency. Nothing is stopping you from injecting your handlers here instead. The mediator pattern isn't adding anything here in terms of dependency injection.

The mediator object itself would ideally be injected by a third party (usually a DI container), and the pattern does not mandate whether or not the handlers are instantiated through a supported DI container or whether the mediator expects to receive already instantiated handlers (or handler factories) which have already resolved all of their dependencies.

You could do a mediator pattern without any kind of DI anywhere in your codebase. I wouldn't advocate doing so, but my reasons for advising so are totally unrelated as to whether you are or aren't making use of the mediator pattern.

What the mediator pattern adds

However, the receiver for this request is actually defined in the same architectural layer (the "Web" layer), leading me to question whether the mediator pattern adds any value beyond dependency injection.

The very purpose of the mediator pattern is to not have to care about who handles the requests. You cannot answer this question by investigating who handles which request, as that's the direct opposite.

For example, you might be building a webshop that also sells alcohol, and for it to figure out if you can sell alcohol to the user, you need to know what the current location's minimum age requirements are (which we're assuming are not set in stone for your application). To achieve this, you create a GetMinimumAgeForAlcohol request. But this doesn't tell you who is the only handling that request.

Consider:

  • Maybe this is hardcoded in a local config file (e.g. web.config). If so, the handler will likely exists in your Web project as it has the concrete knowledge to know how to access its own configuration data.
  • Maybe this is hardcoded in a config, but you've stored this configuration in a remote cloud resource (such as Azure Configuration) or even just a database. This means that it is your Infrastructure layer that will know how to find the information.
  • Maybe your current location is liable to change the minimum age frequently, so the government has provided a web service that tells you the current minimum age that you should implement. This is also Infrastructure, but some codebases prefer to separate "internal" (i.e. application-owned) versus external data sources.
  • Maybe your current location doesn't have a flat minimum age, but it applies a sort of rate-limiting for younger people so that they can't buy alcohol too frequently (this is not fully invented, my country trialed this kind of approach). This means that you need to be able to revisit the customer's purchase history, which pushes the handler to be more in the application/domain side of the codebase.
    • A different example is that the rules for purchasing alcohol might be different on different days (relating to public holidays etc). There is very similar legislation where I'm from in relation to e.g. the sale of fireworks, or whether or not learner drivers are allowed to drive at night depending on whether it's a public holiday. Regardless, these are all just arbitrary example to indicate the variety of ways this request could be answered.
  • Maybe you are building your software to be deployable in all kinds of regions which include all of the above possible scenarios. You've created all these handlers in your codebase, but you've left it so that regardless of which handler is actively in use, the consumer of that mediated request does not have to change anything about what it does.

The specifics don't matter. What matters here is that the need for this "get minimum age" request is independent from the specifics on how you will find the correct response for that request.

The mediator pattern enhances that independence in a way that if the handler changes, this does not have knock-on effect for the consumer of the request, as the request is still just being routed via the same mediator regardless of who the handler is.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.