When a handful of objects all need to react to each other, the easy thing is to give each one a reference to the others. It works, until you try to draw who calls whom and end up with a plate of spaghetti. Mediator replaces that web of connections with a hub: everyone talks to the mediator, and the mediator decides who needs to know.
| Category | Behavioral |
| Smell fixed | S17: Cart widgets update each other directly |
| Code | patterns/behavioral/mediator |
The Scene
I’ll be upfront: CornerStore is a console app, so its “cart widgets” are a bit of a stretch. They exist mainly to give Mediator a home. But they model something every UI developer has met: an item list, a promo box, a shipping selector and a totals panel that all need to stay in sync.
Watch the order of the lines when Ava changes different things:
[030] cart.promo customer=cu-1 code=SHOES10
promo SHOES10
- SHOE-RUN-42 Runner Pro x2 @ A$89.95 = A$179.90
[031] cart.shipping customer=cu-1 method=express
shipping express
- SHOE-RUN-42 Runner Pro x2 @ A$89.95 = A$179.90
[032] cart.addon customer=cu-1 addon=giftwrap
totals A$295.44
- SHOE-RUN-42 Runner Pro x2 @ A$89.95 = A$179.90
Whichever widget changed refreshes first, then everyone else refreshes after it. So where does that rule live?
The Smell
In the baseline, every widget holds a reference to every other widget. Here’s the promo box:
internal class PromoBoxWidget(Cart cart, Customer customer, CartService cartService, PromoEngine promos)
{
private ItemListWidget? itemList;
private ShippingSelectorWidget? shippingSelector;
private TotalsPanelWidget? totalsPanel;
And when it changes, it refreshes its peers itself:
public void RefreshAfterChange()
{
// SMELL S17: this widget refreshes its peers directly after a promo change
Refresh();
itemList?.Refresh();
shippingSelector?.Refresh();
totalsPanel?.Refresh();
}
All four widgets do this. That’s four widgets each knowing about three others: twelve connections. The “refresh myself first, then the others” rule is copied into four classes. Add a fifth widget, say a loyalty points badge, and you have to edit all four existing widgets to tell them it exists.
None of the widgets can be reused or tested on its own, because each one drags its three neighbours along.
The Idea
A Mediator sits in the middle. Each widget (the book calls them colleagues) knows only the mediator. When something changes, a widget tells the mediator “I changed”, and the mediator decides who else needs to react, and in what order.
Think of air traffic control. Planes don’t radio each other to negotiate who lands first. Every pilot talks to the tower, and the tower coordinates everyone. Add another plane to the sky and nobody else’s radio setup changes.
The Refactor
The widgets lose their peer references and their Connect methods entirely. The item list now just reports that it changed:
if (error is null)
{
mediator.ItemListChanged();
}
And the coordination rules live in one class, CartMediator, where you can read all of them side by side:
public void PromoChanged()
{
PromoBox.Refresh();
ItemList.Refresh();
ShippingSelector.Refresh();
TotalsPanel.Refresh();
}
There’s one method like that for each kind of change. The “changed widget refreshes first” rule that used to be scattered across four classes is now four short methods next to each other in one file.
classDiagram
StoreConsole --> CartMediator : sends widget actions
CartMediator o--> ItemListWidget : coordinates
CartMediator o--> PromoBoxWidget : coordinates
CartMediator o--> ShippingSelectorWidget : coordinates
CartMediator o--> TotalsPanelWidget : coordinates
ItemListWidget --> CartMediator : reports item change
PromoBoxWidget --> CartMediator : reports promo change
ShippingSelectorWidget --> CartMediator : reports shipping change
Twelve connections between widgets became four connections to one hub. Adding that loyalty badge now means one new widget and a line in the mediator. The other widgets never hear about it.
What It Cost
8 files: about 235 lines added and 116 removed. Removing all the peer references and Connect methods accounts for most of the deletions. In return, there’s a new class that holds every interaction rule.
When Not to Use It
- Only two objects interact. A mediator between two colleagues is just an extra hop.
- The mediator turns into a god class. This is the big risk. A mediator centralises interaction logic, and if business rules keep landing there too, it becomes the thing everyone is scared to touch. Keep it about coordination, not decisions.
- The objects don’t really depend on each other. If widgets just need to hear about changes and don’t care about order, plain events or Observer might be lighter.
In .NET, you may know the name from the popular MediatR library. It’s inspired by the same idea, sending requests to a central dispatcher instead of calling handlers directly, though it’s used more for decoupling application layers than for coordinating UI widgets.
Easily Confused With
- Facade. Both sit in the middle of several objects. A facade is a front door for outside callers, and the services behind it don’t know it exists. A mediator coordinates peers that know about it and talk through it. See Facade vs Mediator.
- Observer. Both decouple senders from receivers. Observer broadcasts “something happened” to whoever subscribed, without caring what they do. Mediator actively decides who reacts and in what order.
Try It Yourself
dotnet build patterns/behavioral/mediator/Mediator.slnx
dotnet test patterns/behavioral/mediator/Mediator.slnx
dotnet run --project patterns/behavioral/mediator/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json
Exercise: open the four baseline widgets side by side, then open CartMediator.cs. Same rules, but in one file you can read in a minute.
The Mediator README has the participant table and a sequence diagram. The full change is in DIFF.md.