You built a version of this pattern back in Advanced Part II, before it had a name. This lesson gives it one — and closes out the behavioral half of the GoF catalog.
Picture a checkout screen with five moving parts: a product list, a cart summary, a coupon box, a shipping selector, and a "place order" button. Select a product and the cart summary needs to update. Apply a coupon and the cart total needs to recompute, and the shipping selector might need to re-check free-shipping eligibility. Change the shipping method and the total changes again, which might invalidate the coupon.
The tempting, "obvious" way to wire this is for each component to hold direct references to every other component it needs to talk to — the product list holds a reference to the cart summary, the coupon box holds references to both the cart summary and the shipping selector, the shipping selector holds a reference back to the cart summary, and so on. Five components, and already a dozen crisscrossing wires between them. Add a sixth component — a loyalty-points panel — and you're not adding one relationship, you're potentially adding four or five, one to every existing component that now needs to know about it, and vice versa. This is many-to-many coupling, and it gets worse, not better, as the system grows.
In this lesson, you'll learn the Mediator pattern — a behavioral design pattern that replaces that tangled web of direct references with a single, central coordinator, so every component only ever needs to know about the mediator, never about each other.
The Mediator pattern introduces one object — the mediator — that all the other objects talk through, instead of talking to each other directly. When something happens in one component, it doesn't reach out to the other components itself; it tells the mediator, and the mediator decides what needs to happen next and talks to whichever other components are affected. No component holds a reference to any other component — only to the mediator.
Mediator is one of the Gang of Four's behavioral design patterns — the same category as Strategy and Observer from earlier in this Part. The GoF definition: "Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently." The key phrase is "encapsulates how a set of objects interact" — the coordination logic that used to be smeared across every participant, each one calling into a few others, is pulled out and centralized into one place.
The problem Mediator solves is exactly the checkout-screen scenario from the hook, generalized: any time a group of objects needs to coordinate — react to each other's changes, trigger follow-up behavior in one another, stay in sync — the naive solution is for each object to hold direct references to the others. That works for two or three objects. It stops working once the group grows, for reasons that compound:
Two concrete costs of the many-to-many web are worth naming directly. First, reuse: a component wired with direct references to four specific siblings can't easily be dropped into a different screen that doesn't have those exact four siblings — its behavior is entangled with its neighbors' identities. Second, change: adding a sixth component to a five-component web isn't a one-line change, it's a review of every existing component to see which of them now also need a reference to the new one. A mediator turns both of these into non-problems: a component only needs the mediator to function, and a new component only needs to know the mediator too.
ProductList ↔ CartSummary
ProductList ↔ ShippingBox
CouponBox ↔ CartSummary
CouponBox ↔ ShippingBox
ShippingBox ↔ CartSummary
(every pair wired directly —
grows fast as parts are added)
ProductList → CheckoutMediator
CartSummary → CheckoutMediator
CouponBox → CheckoutMediator
ShippingBox → CheckoutMediator
(every part knows only the hub —
a new part adds one spoke)
Notice what didn't change: the same coordination still happens — selecting a product still updates the cart, applying a coupon still recomputes the total. What changed is where that coordination logic lives and how many relationships exist. It moved out of every individual component and into one dedicated place, and the number of wires dropped from roughly one per pair to exactly one per component.
public interface ICheckoutMediator
{
void Notify(object sender, string eventName);
}
Three UI-style components that need to coordinate, with zero references between any two of them:
// ── Mediator abstraction ──
public interface ICheckoutMediator
{
void Notify(object sender, string eventName);
}
// ── Components — each knows ONLY the mediator ──
public class CouponBox(ICheckoutMediator mediator)
{
public void ApplyCoupon(string code)
{
Console.WriteLine($"Coupon '{code}' applied.");
mediator.Notify(this, "CouponApplied");
}
}
public class ShippingSelector(ICheckoutMediator mediator)
{
public bool QualifiesForFreeShipping { get; private set; }
public void RecheckEligibility(decimal cartTotal)
{
QualifiesForFreeShipping = cartTotal >= 50m;
Console.WriteLine($"Free shipping: {QualifiesForFreeShipping}");
}
public void ChangeMethod(string method)
{
Console.WriteLine($"Shipping method changed to {method}.");
mediator.Notify(this, "ShippingMethodChanged");
}
}
public class CartSummary(ICheckoutMediator mediator)
{
public decimal Total { get; private set; } = 62.00m;
public void Recalculate()
{
Console.WriteLine($"Cart total recalculated: {Total:C}");
mediator.Notify(this, "TotalRecalculated");
}
}
// ── The mediator — the ONLY place that knows every component's concrete type ──
public class CheckoutMediator : ICheckoutMediator
{
private CouponBox _coupon = null!;
private ShippingSelector _shipping = null!;
private CartSummary _cart = null!;
public void Register(CouponBox coupon, ShippingSelector shipping, CartSummary cart)
{
_coupon = coupon;
_shipping = shipping;
_cart = cart;
}
public void Notify(object sender, string eventName)
{
switch (eventName)
{
case "CouponApplied":
_cart.Recalculate();
_shipping.RecheckEligibility(_cart.Total);
break;
case "TotalRecalculated":
_shipping.RecheckEligibility(_cart.Total);
break;
case "ShippingMethodChanged":
_cart.Recalculate();
break;
}
}
}
// ── Usage ──
var mediator = new CheckoutMediator();
var coupon = new CouponBox(mediator);
var shipping = new ShippingSelector(mediator);
var cart = new CartSummary(mediator);
mediator.Register(coupon, shipping, cart);
coupon.ApplyCoupon("SAVE10");
// Coupon 'SAVE10' applied.
// Cart total recalculated: $62.00
// Free shipping: TrueCode → Meaning → Result: CouponBox never references CartSummary or ShippingSelector — it only calls mediator.Notify(...). The mediator is the single place deciding that applying a coupon should trigger a recalculation, and that a recalculation should trigger a shipping re-check. Swap in a fourth component tomorrow, and only the mediator's Notify method needs to learn about it — none of the three existing components change.
The Mediator pattern shows up in a different, very common shape in real ASP.NET Core codebases today: an in-process request/handler dispatcher. Instead of a controller or service directly calling a specific handler class, it sends a plain request object to a mediator, and the mediator finds and invokes whichever handler is registered for that request type — the sender never holds a direct reference to the handler. This is precisely the same structural idea as the checkout dialog above (route through a hub instead of a direct reference), applied to command/query dispatch instead of UI coordination.
// ── The request — a plain data object describing intent ──
public record PlaceOrderCommand(int CustomerId, IReadOnlyList<OrderItem> Items) : IRequest<OrderResult>;
// ── The handler — contains the actual logic, but is never called directly by name ──
public class PlaceOrderHandler(IInventoryRepository inventory, IPaymentGateway payments)
: IRequestHandler<PlaceOrderCommand, OrderResult>
{
public async Task<OrderResult> Handle(PlaceOrderCommand command, CancellationToken ct)
{
if (!await inventory.HasStockAsync(command.Items, ct))
return OrderResult.OutOfStock();
var payment = await payments.ChargeAsync(command, ct);
return payment.Success ? OrderResult.Success() : OrderResult.PaymentFailed(payment.Reason);
}
}
// ── The controller — depends ONLY on the mediator, never on PlaceOrderHandler ──
[ApiController]
[Route("orders")]
public class OrdersController(IMediator mediator) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> PlaceOrder(PlaceOrderCommand command)
{
var result = await mediator.Send(command);
return result.Success ? Ok(result) : BadRequest(result);
}
}The controller has no compiled reference to PlaceOrderHandler at all — it depends only on IMediator. This is exactly what the popular MediatR NuGet library provides: you define a request type, a handler for it, and call mediator.Send(request) from wherever the request originates; MediatR's internal dispatcher finds the registered handler and invokes it. This lesson isn't a tutorial on MediatR's specific API — the point is recognizing the shape: a sender, a request object, a mediator that dispatches, and a handler that never has to be referenced directly by its caller. If you see this "send a request, a handler processes it" shape in a real ASP.NET Core codebase, you're looking at the Mediator pattern.
Pilots approaching a busy airport do not radio each other directly to negotiate who lands first, who holds at what altitude, or who taxis where. Every pilot talks to exactly one party: the control tower. The tower knows the position of every aircraft and decides the coordination — "Flight 12, hold at 3,000 feet," "Flight 47, cleared to land." No pilot needs to know how many other flights are in the sky, or track their positions personally; they only need a radio tuned to the tower.
Now imagine the alternative: fifty aircraft, each broadcasting to and listening for every other aircraft directly, each pilot personally negotiating right-of-way with dozens of strangers in real time. It's not just more complicated — past a certain number of aircraft, it becomes genuinely unmanageable and unsafe. The tower doesn't reduce the amount of coordination happening; it concentrates the coordination logic in one place built for exactly that job, and gives every other participant one simple relationship to maintain instead of many.
Both patterns decouple objects from holding direct references to each other, and both are behavioral GoF patterns — that's genuinely why they're easy to confuse. The distinction: Observer is fundamentally one-directional and anonymous — a subject broadcasts a notification, and any number of observers may or may not be listening, with the subject never knowing or caring who they are or what they'll do. Mediator is typically richer and more explicitly coordinating — the mediator usually knows exactly which participants exist, and actively decides what should happen next among them, not just "notify whoever's listening." An event aggregator (192) sits close to Observer's shape (many anonymous publishers and subscribers through a hub) while still being a recognizable Mediator application; a UI dialog mediator or a request/handler dispatcher sits closer to Mediator's classic, more deliberate coordination role. The overlap is real — don't lose sleep over drawing a perfectly sharp line between them.
MediatR is one specific, popular NuGet package that implements a request/handler flavor of the Mediator pattern for .NET. The Mediator pattern itself predates MediatR by decades — it's a general GoF pattern, and MediatR is just one well-known, real-world tool built on top of it. You can (and this lesson did) hand-write a small mediator with no library at all; MediatR simply provides a polished, batteries-included version of the same idea, with conventions for registering handlers, pipeline behaviors, and more.
Routing every interaction in an entire application through one single mediator class, until it accumulates coordination logic for dozens of unrelated features and becomes a sprawling, impossible-to-navigate class of its own.
Scope a mediator to a genuinely cohesive group of collaborating objects — one screen, one workflow, one bounded piece of coordination. Large applications often have several small, focused mediators (or, in the request/handler style, one dispatcher with many small, focused handlers) rather than a single mediator for everything.
Introducing an ICheckoutMediator and a Notify dispatch mechanism for a relationship that's just two specific classes calling one specific method on each other, with no third party ever involved and no plausible growth beyond two.
A direct method call or a plain constructor dependency is simpler, easier to trace with "Find References," and entirely appropriate when the relationship really is just two parties. Mediator earns its cost once there are three or more participants whose interactions would otherwise cross-wire.
Believing that introducing a mediator has somehow eliminated coupling from the system entirely, rather than concentrated it in one place — and then being surprised the mediator class itself is complex.
Expect the mediator to be the one class that legitimately knows about every participant — that's its job. The win isn't "zero coupling anywhere," it's "coupling that used to be scattered across N classes is now readable in one place, and every individual participant is simpler and more reusable than before."
| Signal | Leans toward |
|---|---|
| Three or more objects that need to coordinate, with interactions that would otherwise cross-wire many-to-many | A mediator — this is the pattern's core use case |
| Controllers/handlers that would otherwise need direct references to many different service classes for many different operations | A request/handler dispatcher (MediatR or hand-rolled) as the mediator |
| Independent, unrelated modules across a large application reacting to shared facts | An event aggregator (192) — a Mediator-shaped, pub/sub-flavored solution |
| Exactly two objects, with a stable, simple, direct relationship and no plausible third party | Skip it — a direct reference or constructor dependency is simpler and just as testable |
| A single class already accumulating coordination logic for many unrelated features | Split it into several smaller, focused mediators rather than growing one further |
You've seen the Mediator pattern in a UI-coordination shape and a request/handler shape, and connected it back to the event aggregator. Let's confirm the reasoning sticks.
1. What specific coupling problem does the Mediator pattern solve?
Correct: B
Why B is correct: This is the pattern's defining structural change — participants that would otherwise hold direct references to several other participants instead hold a reference only to the mediator, and the mediator holds references to all of them.
Why A is incorrect: Mediator doesn't eliminate interfaces — the mediator itself is commonly defined behind an interface (as ICheckoutMediator was), and participants may still implement contracts of their own.
Why C is incorrect: Mediator is about object composition and message routing, not inheritance — no common base class is required or implied.
Why D is incorrect: The coordination logic still exists — it's relocated into the mediator, not removed. The lesson was explicit that coupling moves rather than vanishes.
Reinforcement: Mediator's value is structural — fewer, more manageable relationships — not the removal of coordination logic itself.
2. In the checkout dialog example, why does CouponBox call mediator.Notify(this, "CouponApplied") instead of calling cartSummary.Recalculate() directly?
Correct: B
Why B is correct: The entire point of the pattern is that CouponBox doesn't need to know CartSummary exists. It reports its own event; the mediator — the one class allowed to know every participant — decides what should happen as a result.
Why A is incorrect: C# has no such restriction; direct method calls between classes are entirely normal and were exactly what the "without a mediator" design used.
Why C is incorrect: The example's Recalculate() is a public method; the issue isn't accessibility, it's that CouponBox should not be coupled to CartSummary's type at all.
Why D is incorrect: The lesson makes no performance claim about the mediator — the motivation is coupling and maintainability, not runtime speed.
Reinforcement: Participants report facts to the mediator; only the mediator decides what those facts mean for everyone else.
3. How does the Event Aggregator from lesson 192 relate to the Mediator pattern covered here?
Correct: B
Why B is correct: The lesson connects them directly — an event aggregator's Publish/Subscribe hub is a Mediator-shaped solution specialized for publish/subscribe coordination, built on the same idea of routing through a shared object instead of direct references.
Why A is incorrect: The event aggregator is a design pattern application for decoupling publishers from subscribers, not a threading primitive — it was covered as such in lesson 192.
Why C is incorrect: The event aggregator is one flavor of Mediator-shaped solution (pub/sub), not a universal replacement — a UI dialog mediator or a request/handler dispatcher are other valid shapes for different coordination needs.
Why D is incorrect: Mediator is a behavioral pattern, not creational, and the event aggregator shares that same behavioral concern — coordinating interaction between objects, not object creation.
Reinforcement: Recognizing the same underlying shape across different-looking tools — an event aggregator, a UI mediator, a request dispatcher — is the real skill this lesson is building.
4. A team routes every interaction across an entire large application — from unrelated features spanning dozens of screens — through one single mediator class, which has grown to thousands of lines. What does this lesson identify as the problem here?
Correct: B
Why B is correct: This is Common Mistake 1 directly — a single mediator absorbing coordination logic for unrelated features across an entire application becomes an unmanageable God object. The fix is scoping mediators (or handlers, in the dispatch style) to cohesive groups.
Why A is incorrect: A growing, unfocused mediator is exactly the failure mode the lesson warns against — bigger is not automatically better.
Why C is incorrect: Mediator has no fixed participant count — the guidance is "three or more" as a general signal, not an exact ceiling.
Why D is incorrect: MediatR is one popular way to implement a request/handler style mediator, but the pattern itself requires no specific library — the hand-rolled checkout example used none.
Reinforcement: Scope matters — many small, focused mediators (or handlers) generally beat one sprawling mediator for an entire application.
You've now completed the GoF pattern catalog for this Part — Factory, Strategy, Decorator, Adapter, Observer, Builder, and now Mediator. Next, the focus shifts from individual reusable patterns to the larger structural questions of how a whole application's data access and architecture should be organized, starting with the Repository pattern.
dotnetmadeeasy.com — Learn C# and .NET, the right way.