BrandGhost
When to Use Mediator Pattern in C#: Decision Guide with Examples

When to Use Mediator Pattern in C#: Decision Guide with Examples

When to Use Mediator Pattern in C#: Decision Guide with Examples

As your codebase grows, objects start talking to each other directly. A lot. Before you know it, every component holds references to three, five, or ten other components -- and changing any one of them means updating all the others. The when to use mediator pattern in C# question surfaces the moment you realize that your objects are tangled in a web of many-to-many dependencies that makes refactoring feel dangerous and testing feel impossible.

This article gives you a structured decision framework for recognizing when a mediator is the right tool and -- just as importantly -- when it introduces unnecessary indirection. We'll walk through the signs that your code needs one, the scenarios where it fits naturally, and the situations where it's overkill. You'll see before-and-after code examples covering UI form coordination, chat systems, and workflow orchestration. By the end, you'll have a practical checklist for deciding whether this approach belongs in your architecture or whether simpler alternatives like direct references or the observer pattern serve you better.

What the Mediator Pattern Actually Does

The mediator pattern introduces a central object that encapsulates how a set of objects interact. Instead of components communicating directly with each other, they communicate through this intermediary. Each component knows about the mediator, but it doesn't know about the other components. The mediator knows about all the components and coordinates their interactions.

Think of it like an air traffic control tower. Planes don't communicate directly with each other to negotiate landing order and runway assignments. They communicate with the tower, and the tower coordinates everything. Each plane is simpler because it only needs to talk to one entity. The complexity of coordination lives in the tower -- the mediator -- rather than being distributed across every participant.

The standard structure involves three roles. A mediator interface declares methods for communication between components. A concrete mediator implements the coordination logic and holds references to all the participating components. And colleague objects are the components that interact through the mediator rather than directly. Each colleague holds a reference to the mediator interface, not to other colleagues. This is a concrete application of inversion of control -- colleagues depend on an abstraction rather than on each other.

Signs Your Code Needs a Mediator

Not every set of interacting objects needs a mediator. But certain patterns in your codebase are strong signals that this approach can simplify your architecture and reduce coupling.

You Have Many-to-Many Dependencies Between Components

This is the classic symptom. When multiple objects each need to communicate with multiple other objects, you get an explosion of direct references. A form with ten controls where each control's state change affects several other controls creates dozens of pairwise dependencies. Adding a new control means modifying existing controls to accommodate it. Removing a control means hunting down every reference to it. A mediator collapses this N-to-N relationship into N-to-1 -- every component talks to the coordinator, and the coordinator handles the rest.

Changing One Component Forces Changes in Many Others

If modifying a single class requires you to update three, four, or five other classes that reference it, your components are too tightly coupled. A mediator breaks these direct dependencies. When you change a component's behavior, you update the coordination logic instead of updating every other component that interacts with it. The ripple effect of changes stays contained.

You Can't Reuse Components Independently

A well-designed component should be reusable in different contexts. If your DatePicker control can only work when it has references to a StartDateValidator, a EndDateField, and a SubmitButton, it's not reusable -- it's welded into one specific form. A mediator frees components from these direct dependencies, making them portable across different configurations. You can also lean on dependency injection to wire colleagues to their coordinator, keeping your component graph flexible.

Your Object Interaction Logic Is Scattered Across Multiple Classes

When the rules for "what happens when X changes" are spread across five different classes, understanding the system's behavior requires reading all of them. A mediator centralizes interaction logic in one place. You can read it to understand how components coordinate without jumping between a dozen files.

Scenarios Where a Mediator Fits

This pattern shines in specific categories of problems. Here's where it earns its weight in complexity.

Chat Room or Messaging Systems

A chat room is a natural mediator. Users send messages to the chat room, and the chat room distributes them to other participants. Without a mediator, each user would need references to every other user -- and that list changes as people join and leave. The chat room mediator handles participant management and message routing in one place.

UI Form Coordination

Complex forms often have interdependent controls. Selecting a country changes the available states. Enabling "custom shipping" reveals additional fields and disables the standard shipping dropdown. Checking "same as billing address" copies values and disables shipping fields. When every control directly manipulates other controls, the form becomes a tangled mess. A form mediator centralizes these interaction rules, and each control simply notifies the mediator when its value changes.

Workflow Orchestration

Multi-step workflows where each step's behavior depends on the outcome of previous steps benefit from a mediator. An order processing workflow might involve inventory checks, payment processing, shipping calculation, and notification. Each service communicates through the mediator, which coordinates the flow. This approach differs from the chain of responsibility pattern where handlers pass requests along a chain -- a mediator actively coordinates rather than passively forwarding.

Event Aggregation

When multiple publishers and multiple subscribers need to communicate, an event aggregator acts as a mediator. Components publish events to the aggregator, and the aggregator routes them to interested subscribers. This is particularly useful in plugin architectures, modular applications, and scenarios where publishers and subscribers are loaded dynamically and shouldn't know about each other.

When a Mediator Is Overkill

Knowing when to use the mediator pattern in C# is only half the picture. Reaching for it in situations where simpler approaches work adds a layer of indirection that obscures your code without reducing complexity.

Simple One-to-One Relationships

If component A only talks to component B, introducing a mediator between them adds an extra class and an extra level of indirection for no benefit. This pattern solves many-to-many communication problems. For one-to-one relationships, a direct reference or a simple interface is cleaner and easier to understand. You might consider the adapter pattern if the issue is interface incompatibility rather than coupling complexity.

The Mediator Becomes a God Object

This is the biggest risk. If you centralize all coordination logic into a single mediator, that mediator can grow into an enormous class that knows about every component and every interaction rule in your system. A mediator with fifty if statements and references to twenty components isn't simplifying anything -- it's moving complexity from the edges to the center. If your mediator is turning into a god object, consider splitting it into multiple focused mediators or reconsider whether the pattern is appropriate.

You Only Have Two or Three Interacting Components

The mediator pattern's value scales with the number of interacting components. With two components, you have one direct dependency. With five components fully interconnected, you have ten. With ten, you have forty-five. A mediator collapses N*(N-1)/2 dependencies into N. When N is small, the savings don't justify the indirection. Wait until the dependency count is genuinely painful before introducing one.

Direct Communication Is Clear and Stable

If components communicate through well-defined, stable interfaces and the interaction logic is simple, a mediator adds indirection without adding value. The facade pattern might be a better fit if you simply want to provide a simpler interface to a set of subsystems without restructuring how they interact.

Code Example: Before and After Refactoring to a Mediator

Let's look at a practical scenario -- a UI form with interdependent controls -- to see how introducing a mediator transforms tightly coupled code into a coordinated system.

Before: Tightly Coupled Form Controls

In this version, each control holds direct references to the other controls it affects. Changing the country dropdown directly updates the state dropdown and the shipping options:

// Tightly coupled: controls reference each other directly
public sealed class CountryDropdown
{
    private readonly StateDropdown _stateDropdown;
    private readonly ShippingOptionsPanel _shippingPanel;
    private readonly TaxCalculator _taxCalculator;

    public CountryDropdown(
        StateDropdown stateDropdown,
        ShippingOptionsPanel shippingPanel,
        TaxCalculator taxCalculator)
    {
        _stateDropdown = stateDropdown;
        _shippingPanel = shippingPanel;
        _taxCalculator = taxCalculator;
    }

    public string SelectedCountry { get; private set; } = "";

    public void SelectCountry(string country)
    {
        SelectedCountry = country;

        // Directly manipulating other controls
        _stateDropdown.LoadStatesFor(country);
        _shippingPanel.UpdateOptionsFor(country);
        _taxCalculator.SetRegion(country);
    }
}

public sealed class StateDropdown
{
    private readonly ShippingOptionsPanel _shippingPanel;
    private readonly TaxCalculator _taxCalculator;

    public StateDropdown(
        ShippingOptionsPanel shippingPanel,
        TaxCalculator taxCalculator)
    {
        _shippingPanel = shippingPanel;
        _taxCalculator = taxCalculator;
    }

    public string SelectedState { get; private set; } = "";

    public void LoadStatesFor(string country)
    {
        Console.WriteLine(
            $"Loading states for {country}...");
        SelectedState = "";
    }

    public void SelectState(string state)
    {
        SelectedState = state;

        // More direct manipulation
        _shippingPanel.UpdateOptionsFor(state);
        _taxCalculator.SetSubRegion(state);
    }
}

Every control knows about every other control it affects. Adding a new control -- say, a CurrencyDisplay that should update when the country changes -- means modifying CountryDropdown to add another dependency. The controls cannot be reused in a different form because they're hardwired to specific collaborators.

After: Mediator-Coordinated Form Controls

With a mediator in place, each control only knows about the coordinator. The concrete mediator contains the coordination logic:

public interface IFormMediator
{
    void Notify(object sender, string eventName);
}

// Colleague base class
public abstract class FormControl
{
    protected IFormMediator Mediator { get; }

    protected FormControl(IFormMediator mediator)
    {
        Mediator = mediator;
    }
}

Each control becomes self-contained:

public sealed class CountryDropdown : FormControl
{
    public CountryDropdown(IFormMediator mediator)
        : base(mediator) { }

    public string SelectedCountry { get; private set; } = "";

    public void SelectCountry(string country)
    {
        SelectedCountry = country;
        Mediator.Notify(this, "CountryChanged");
    }
}

public sealed class StateDropdown : FormControl
{
    public StateDropdown(IFormMediator mediator)
        : base(mediator) { }

    public string SelectedState { get; private set; } = "";

    public void LoadStatesFor(string country)
    {
        Console.WriteLine(
            $"Loading states for {country}...");
        SelectedState = "";
    }

    public void SelectState(string state)
    {
        SelectedState = state;
        Mediator.Notify(this, "StateChanged");
    }
}

public sealed class ShippingOptionsPanel : FormControl
{
    public ShippingOptionsPanel(IFormMediator mediator)
        : base(mediator) { }

    public void UpdateOptionsFor(string region)
    {
        Console.WriteLine(
            $"Updating shipping options for {region}...");
    }
}

public sealed class TaxCalculator : FormControl
{
    public TaxCalculator(IFormMediator mediator)
        : base(mediator) { }

    public void SetRegion(string country)
    {
        Console.WriteLine(
            $"Setting tax region to {country}...");
    }

    public void SetSubRegion(string state)
    {
        Console.WriteLine(
            $"Setting tax sub-region to {state}...");
    }
}

The mediator centralizes the coordination rules:

public sealed class CheckoutFormMediator : IFormMediator
{
    public CountryDropdown Country { get; set; } = null!;
    public StateDropdown State { get; set; } = null!;
    public ShippingOptionsPanel Shipping { get; set; } = null!;
    public TaxCalculator Tax { get; set; } = null!;

    public void Notify(object sender, string eventName)
    {
        if (sender is CountryDropdown country
            && eventName == "CountryChanged")
        {
            State.LoadStatesFor(country.SelectedCountry);
            Shipping.UpdateOptionsFor(
                country.SelectedCountry);
            Tax.SetRegion(country.SelectedCountry);
        }

        if (sender is StateDropdown state
            && eventName == "StateChanged")
        {
            Shipping.UpdateOptionsFor(
                state.SelectedState);
            Tax.SetSubRegion(state.SelectedState);
        }
    }
}

Now adding a CurrencyDisplay means creating one new class and adding two lines to the mediator's Notify method. No existing controls change. Each control is reusable because it only depends on IFormMediator, not on specific siblings. The interaction logic lives in one place -- the mediator -- where you can read, test, and modify it without touching the controls themselves.

Code Example: Chat Room Mediator

A chat room demonstrates this pattern with dynamic participants. Users join, leave, and send messages through the mediator:

public interface IChatMediator
{
    void Register(ChatUser user);
    void SendMessage(
        string message,
        ChatUser sender);
    void SendDirectMessage(
        string message,
        ChatUser sender,
        string recipientName);
}

public sealed class ChatUser
{
    private readonly IChatMediator _mediator;

    public ChatUser(
        string name,
        IChatMediator mediator)
    {
        Name = name;
        _mediator = mediator;
    }

    public string Name { get; }

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

    public void SendTo(string message, string recipient)
    {
        Console.WriteLine(
            $"{Name} sends to {recipient}: {message}");
        _mediator.SendDirectMessage(
            message, this, recipient);
    }

    public void Receive(string message, string from)
    {
        Console.WriteLine(
            $"{Name} receives from {from}: {message}");
    }
}

The concrete mediator manages participants and routes messages:

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

    public void Register(ChatUser user)
    {
        if (!_users.Contains(user))
        {
            _users.Add(user);
            Console.WriteLine(
                $"{user.Name} joined the chat room.");
        }
    }

    public void SendMessage(
        string message,
        ChatUser sender)
    {
        foreach (var user in _users)
        {
            if (user != sender)
            {
                user.Receive(message, sender.Name);
            }
        }
    }

    public void SendDirectMessage(
        string message,
        ChatUser sender,
        string recipientName)
    {
        var recipient = _users.FirstOrDefault(
            u => u.Name == recipientName);

        if (recipient is not null)
        {
            recipient.Receive(message, sender.Name);
        }
    }
}

// Usage
var chatRoom = new ChatRoom();

var alice = new ChatUser("Alice", chatRoom);
var bob = new ChatUser("Bob", chatRoom);
var charlie = new ChatUser("Charlie", chatRoom);

chatRoom.Register(alice);
chatRoom.Register(bob);
chatRoom.Register(charlie);

alice.Send("Hello everyone!");
bob.SendTo("Hey Alice, how's the project?", "Alice");

Without the mediator, each user would need a list of every other user. Adding and removing participants would mean updating every existing user's contact list. The pattern makes participant management the chat room's responsibility, not each user's.

Decision Criteria Checklist

When evaluating whether a mediator fits your situation, walk through these criteria. The more "yes" answers you get, the stronger the case for using it.

Are there more than three components that interact with each other? The pattern's value scales with the number of interconnected components. With two or three, direct references are manageable. Beyond that, the dependency count grows quadratically.

Are the interaction rules complex or frequently changing? If the rules for "when A changes, update B and C but not D" are complex or evolve over time, centralizing them in a mediator makes them easier to understand and modify. Scattered interaction logic is harder to maintain.

Do you need to reuse components in different contexts? If components should work in different configurations -- the same DatePicker in a booking form and a report filter -- you can swap mediators without changing the components.

Is testability a concern? A mediator improves testability. You can test each component in isolation by mocking the mediator. You can test the coordination logic by mocking the components. Direct dependencies make unit testing harder because you need to instantiate the entire dependency graph.

Would a simpler pattern solve the problem? Before committing to a mediator, consider whether the command pattern or a simple event-based approach solves your specific problem with less overhead. The mediator pattern is a structural commitment -- make sure it's justified.

Can you keep the mediator focused? If the mediator would need to coordinate thirty components with fifty interaction rules, it will become a god object. Consider whether you can decompose the system into smaller groups, each with its own mediator, before applying the pattern.

Mediator vs Alternatives: When to Choose What

Understanding when to use the mediator pattern in C# means knowing how it compares to similar patterns that also manage communication between objects.

Criteria Mediator Observer Facade Direct References
Primary purpose Coordinate complex interactions between peers Notify subscribers of state changes Simplify access to a subsystem Connect objects directly
Communication direction Bidirectional through central hub One-to-many broadcast Client-to-subsystem Point-to-point
Coupling Components coupled to mediator only Publishers coupled to event interface Clients coupled to facade Components coupled to each other
Best for Many-to-many interactions One-to-many notifications Simplifying complex APIs Simple, stable relationships
Risk God object mediator Event storms Leaky abstraction Spaghetti dependencies

The mediator pattern and the observer pattern both reduce direct coupling between components, but they solve different shapes of the problem. The observer is ideal when one component's state change needs to notify multiple listeners -- it's a broadcast mechanism. A mediator is better when multiple components need to coordinate with each other based on complex rules -- it's a coordination mechanism. In practice, a mediator might use observer-style events internally to receive notifications from colleagues.

The facade pattern also centralizes access to multiple objects, but it serves a different purpose. A facade simplifies the interface to a subsystem for external clients. A mediator coordinates interactions between peer objects within the subsystem. A facade is about simplification from the outside. A mediator is about decoupling from the inside.

Frequently Asked Questions

What is the mediator pattern in C# and when should I use it?

The mediator pattern is a behavioral design pattern that defines an object to encapsulate how a set of objects interact. Use it when you have multiple components that communicate with each other in complex ways, creating a web of many-to-many dependencies. It replaces direct component-to-component references with a central coordinator, reducing coupling and making components independently reusable and testable.

How does the mediator pattern reduce coupling in C#?

Without a mediator, N interconnected components create up to N*(N-1)/2 direct dependencies. Each component holds references to every component it interacts with. A mediator replaces these pairwise dependencies with N dependencies -- one from each component to the coordinator. Components no longer know about each other. They only know about the mediator interface. This means you can add, remove, or modify components without cascading changes through the rest of the system.

What is the difference between the mediator pattern and the observer pattern?

The observer pattern is a one-to-many broadcast mechanism -- one subject notifies multiple observers when its state changes. The mediator pattern is a many-to-many coordination mechanism -- multiple components communicate through a central coordinator that contains the interaction logic. Use the observer when you need notifications. Use a mediator when you need coordinated interactions between peers. A mediator can internally use observer-style notifications to receive updates from colleagues.

Can the mediator pattern be combined with dependency injection in C#?

Yes. Register the mediator interface and its concrete implementation in your DI container, and inject it into each colleague component through constructor injection. This keeps components loosely coupled to the mediator abstraction and makes it easy to swap mediator implementations for different contexts or testing scenarios.

What are the downsides of the mediator pattern?

The primary risk is the god object problem. If the mediator accumulates too much coordination logic, it becomes an enormous class that's difficult to maintain and test. It can also obscure the flow of communication -- when every interaction goes through an intermediary, it can be harder to trace how components affect each other during debugging. Finally, the mediator introduces a single point of failure -- if it has a bug in its coordination logic, every interaction it manages is affected.

How does the mediator pattern differ from the facade pattern?

The facade pattern provides a simplified interface to a subsystem for external clients. The mediator pattern coordinates interactions between peer objects within the subsystem. A facade is about simplifying access from outside -- clients call the facade instead of calling subsystem components directly. A mediator is about decoupling interactions on the inside -- components communicate through the mediator instead of referencing each other directly.

Is MediatR the same as the mediator pattern?

MediatR is a popular .NET library that implements a specific flavor of the mediator pattern focused on in-process message passing. It routes commands and queries to their handlers through a central dispatcher. While it's built on the mediator concept, the classic pattern is broader -- it applies to any scenario where you need a central coordinator for component interactions, not just request/handler dispatching. You can implement a mediator without any library by defining your own interface and concrete class, as shown in the examples throughout this article.

Wrapping Up the Mediator Pattern Decision Guide

Deciding when to use the mediator pattern in C# comes down to the shape of your component interactions. If you have multiple objects tangled in many-to-many dependencies, if changing one component forces changes in several others, and if your interaction logic is scattered across too many classes -- a mediator gives you a clean way to centralize coordination and reduce coupling.

The decision checklist is practical. Count the interconnected components. Assess whether the interaction rules are complex or volatile. Check whether you need component reusability. And watch for the god object risk -- a mediator that tries to coordinate everything becomes the very problem it was supposed to solve. Consider splitting large mediators into smaller, focused ones.

Start with direct references. They're simple, obvious, and often sufficient. When you notice your dependency graph becoming a web rather than a tree -- when adding a new component means modifying five existing ones -- that's your signal to reach for the mediator pattern. Keep the mediator focused on coordination, keep your colleagues simple and independent, and let the pattern earn its place through the decoupling it enables rather than applying it preemptively.

How to Implement Mediator Pattern in C#: Step-by-Step Guide

How to implement the mediator pattern in C# with step-by-step code examples, best practices, and common pitfalls for behavioral design patterns.

Exploring Examples Of The Mediator Pattern In C#

Let's explore examples of the Mediator Pattern in C#! See how the Mediator Pattern in C# can promote loose coupling, and increase scalability.

Mediator Design Pattern in C#: Complete Guide with Examples

Master the mediator design pattern in C# with code examples, real-world scenarios, and best practices for reducing object coupling.

An error has occurred. This application may no longer respond until reloaded. Reload