DEV Community

Cover image for Design Patterns #9: A Hands-On Comparison of Mediator Pattern Approaches
Serhii Korol
Serhii Korol

Posted on • Edited on

Design Patterns #9: A Hands-On Comparison of Mediator Pattern Approaches

Today, let’s explore the Mediator design pattern—an approach that promotes loose coupling between components by centralizing their communication. This pattern becomes especially relevant now that the popular MediatR library has switched to a commercial license. While many companies can still use it for free, the license terms could change in the future. So, it’s wise to be prepared with alternatives.

That said, licensing concerns shouldn't discourage you from using the Mediator pattern itself - it remains a powerful architectural tool.

🤔 What Is the Mediator Pattern?

The Mediator is a behavioral design pattern that allows components (often called colleagues) to communicate through a central mediator rather than referencing each other directly.

In traditional designs, components talk to one another directly:

direct

Over time, this tight coupling creates a tangled web of dependencies, making the system hard to change and test. The Mediator pattern addresses this by introducing a central object to handle all communication:

mediator

🧪 Implementing the Mediator Pattern in Code

Let’s consider a simple chat application. We’ll use the Mediator pattern to decouple users from each other.

Step 1: Define the Mediator Interface

public interface IChatMediator
{
    void SendMessage(string message, User sender);
    void AddUser(User user);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Concrete Mediator (Chat Room)

The chat room will be the Mediator that has two concrete methods that are implementing adding a user to the chat room and sending a message.

public class ChatRoom : IChatMediator
{
    private readonly List<User> _users = new();

    public void AddUser(User user)
    {
        _users.Add(user);
    }

    public void SendMessage(string message, User sender)
    {
        foreach (var user in _users)
        {
            if (user != sender)
            {
                user.ReceiveMessage(message, sender);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Define the User

Let's also create a user who can initiate sending and receiving messages.

public class User
{
    private readonly IChatMediator _mediator;
    private string Name { get; }

    public User(string name, IChatMediator mediator)
    {
        Name = name;
        _mediator = mediator;
        _mediator.AddUser(this);
    }

    public void Send(string message)
    {
        Console.WriteLine($"{Name} sends: {message}");
        _mediator.SendMessage(message, this);
    }

    public void ReceiveMessage(string message, User sender)
    {
        Console.WriteLine($"{Name} receives from {sender.Name}: {message}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Run the Example

Let's use it, create more users, and send messages.

    public void MediatorApproach()
    {
        IChatMediator chatRoom = new ChatRoom();

        var user1 = new User("Alice", chatRoom);
        var user2 = new User("Bob", chatRoom);
        var user3 = new User("Charlie", chatRoom);

        user1.Send("Hello everyone!");
        user2.Send("Hi Alice!");
        user3.Send("Hey folks!");
    }
Enter fullscreen mode Exit fullscreen mode

If you run it, you'll see this result.

Alice sends: Hello everyone!
Bob receives from Alice: Hello everyone!
Charlie receives from Alice: Hello everyone!
Bob sends: Hi Alice!
Alice receives from Bob: Hi Alice!
Charlie receives from Bob: Hi Alice!
Charlie sends: Hey folks!
Alice receives from Charlie: Hey folks!
Bob receives from Charlie: Hey folks!

Process finished with exit code 0.
Enter fullscreen mode Exit fullscreen mode

Communication occurs through the chat room, also known as the Mediator.

🧩 Using MediatR Instead

As I mentioned earlier, there is an ultimate solution, and you can use the NuGet package MediatR. This implementation is more straightforward. MediatR provides a clean and popular solution. You need to implement the notification class and handler.

public class ChatMessage(string sender, string message) : INotification
{
    public string Sender { get; } = sender;
    public string Message { get; } = message;
}

public class ChatRoomHandler : INotificationHandler<ChatMessage>
{
    private static readonly List<string> Users = ["Alice", "Bob", "Charlie"];

    public Task Handle(ChatMessage notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"{notification.Sender} sends: {notification.Message}");
        foreach (var user in Users.Where(user => user != notification.Sender))
        {
            Console.WriteLine($"{user} receives from {notification.Sender}: {notification.Message}");
        }

        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach requires only two interface implementations and the addition of specific logic. As a rule, each handler is responsible for one action and also has its own notification. In an extensive application, it leads to a large number of messages and handlers. However, each pair of a message and a handler is isolated and doesn't affect other handlers.

The use is also different. You must register MediatR. Next, you should publish only messages. MedaitR by type will call the target handler.

public static async Task MediatrApproach()
    {
        var services = new ServiceCollection();

        services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());

        var provider = services.BuildServiceProvider();
        var mediator = provider.GetRequiredService<IMediator>();

        await mediator.Publish(new ChatMessage("Alice", "Hello everyone!"));
        await mediator.Publish(new ChatMessage("Bob", "Hi Alice!"));
        await mediator.Publish(new ChatMessage("Charlie", "Hey folks!"));
    }

Enter fullscreen mode Exit fullscreen mode

🚫 When Not to Use a Mediator

If your app is small, unlikely to scale, or contains only a few components, you might not need a Mediator at all.

Here’s a naive implementation using direct coupling:

public class ChatRoom
{
    private readonly List<User> _users = [];

    public void RegisterUser(User user)
    {
        _users.Add(user);
    }

    public void Broadcast(string message, User sender)
    {
        foreach (var user in _users)
        {
            if (user != sender)
            {
                user.ReceiveMessage(message, sender);
            }
        }
    }
}

public class User
{
    private string Name { get; }
    private readonly ChatRoom _chatRoom;

    public User(string name, ChatRoom chatRoom)
    {
        Name = name;
        _chatRoom = chatRoom;
        _chatRoom.RegisterUser(this);
    }

    public void Send(string message)
    {
        Console.WriteLine($"{Name} sends: {message}");
        _chatRoom.Broadcast(message, this);
    }

    public void ReceiveMessage(string message, User sender)
    {
        Console.WriteLine($"{Name} received from {sender.Name}: {message}");
    }
}
Enter fullscreen mode Exit fullscreen mode

The use of this approach is similar to that of Mediator. You create the chat room, new users, and send messages. The code is almost the same.

    public static void NaiveApproach()
    {
        var chatRoom = new MediatorPattern.Naive.ChatRoom();

        var alice = new MediatorPattern.Naive.User("Alice", chatRoom);
        var bob = new MediatorPattern.Naive.User("Bob", chatRoom);
        var charlie = new MediatorPattern.Naive.User("Charlie", chatRoom);

        alice.Send("Hello everyone!");
        bob.Send("Hi Alice!");
        charlie.Send("Hey folks!");
    }
Enter fullscreen mode Exit fullscreen mode

📊 What About Performance?

As you can guess, the difference between the first and third approaches is minimal. The key difference is only in maintainability and scalability. However, how can a third-party library affect performance? You don't know what was done under the hood, where you don't have control.

benchmark

As expected, third-party libraries like MediatR do introduce slight overhead.

✅ Conclusion

The Mediator pattern is not about performance - it’s about maintainability, modularity, and scalability.

✅ Use the naive approach for quick prototypes or small apps.
✅ Implement your own Mediator for full control and clarity.
✅ Use MediatR for larger applications or when you want to embrace a fully decoupled architecture - but keep the licensing in mind.

☕ If you liked this post, consider supporting me:
Buy Me A Beer

Top comments (1)

Collapse
 
stevsharp profile image
Spyros Ponaris

How do you handle complex workflows (like sagas) with the Mediator pattern?

Can I use Mediator for domain events as well or should I separate it from application events?