Mediator
Think of an air traffic control tower. Airplanes don't radio each other directly to coordinate landings and takeoffs — every plane talks only to the tower, and the tower decides who goes next. Planes never need a direct line to every other plane in the sky. The Mediator pattern applies that same idea to objects in code: introduce a mediator object that all the components talk to instead of each other, so components never need to hold direct references to one another at all.
⚠️ The Problem
Imagine a dialog box with a ListBox, a Label, a
TextBox, and a Button. Selecting an item in the ListBox should update
the Label, clear the TextBox, and enable the Button. Typing in the TextBox should also enable
the Button. The naive approach: give each component a direct reference to every other component
it needs to affect — listBox.onSelect = () => { label.setText(...); textBox.clear();
button.enable(); }, and similar wiring inside the TextBox. Now every component is
tangled up with several others in a many-to-many web, and adding one more field to the dialog
means touching multiple existing components just to wire up the new relationships.
✅ The Solution
Introduce a DialogMediator that all four components report to instead of
talking to each other. Each component only knows about the mediator: the ListBox calls
mediator.notify(this, "selected"), the TextBox calls
mediator.notify(this, "changed"). The mediator holds references to all the
components and decides what should happen in response — updating the Label, clearing the
TextBox, enabling the Button. Components no longer reference each other at all; all the
coordination logic lives in one place, and adding a new component means updating the mediator,
not every existing component.
Example 1 — a dialog box mediator
class DialogMediator {
constructor({ listBox, label, textBox, button }) {
this.listBox = listBox;
this.label = label;
this.textBox = textBox;
this.button = button;
listBox.mediator = this;
textBox.mediator = this;
}
notify(sender, event) {
if (sender === this.listBox && event === "selected") {
this.label.setText(`Selected: ${this.listBox.selectedValue}`);
this.textBox.clear();
this.button.enable();
}
if (sender === this.textBox && event === "changed") {
this.button.setEnabled(this.textBox.value.length > 0);
}
}
}
class ListBox {
selectedValue = null;
mediator = null;
select(value) {
this.selectedValue = value;
this.mediator.notify(this, "selected");
}
}
class TextBox {
value = "";
mediator = null;
type(text) {
this.value = text;
this.mediator.notify(this, "changed");
}
clear() {
this.value = "";
}
}
class Label {
setText(text) { console.log(`Label: "${text}"`); }
}
class Button {
enabled = false;
enable() { this.enabled = true; console.log("Button enabled"); }
setEnabled(v) { this.enabled = v; console.log(`Button enabled: ${v}`); }
}
const listBox = new ListBox();
const textBox = new TextBox();
new DialogMediator({ listBox, label: new Label(), textBox, button: new Button() });
listBox.select("Invoice #4521"); // Label updates, TextBox clears, Button enables
ListBox and TextBox never hold a reference to Label or
Button — they only know about the mediator. All the "who reacts to what" logic lives
in one DialogMediator.notify() method, so it's the only place you need to look (or
change) when the dialog's behavior needs to evolve.
Example 2 — a secondary context: an air traffic control tower
class ControlTower:
def __init__(self):
self._runway_occupied = False
def request_landing(self, airplane):
if self._runway_occupied:
print(f"{airplane.name}: hold — runway occupied.")
airplane.hold()
else:
self._runway_occupied = True
print(f"{airplane.name}: cleared to land.")
airplane.land()
def notify_landed(self, airplane):
print(f"{airplane.name}: has landed, runway now clear.")
self._runway_occupied = False
class Airplane:
def __init__(self, name, tower):
self.name = name
self.tower = tower
def request_landing(self):
self.tower.request_landing(self)
def hold(self):
print(f"{self.name}: circling, awaiting clearance.")
def land(self):
print(f"{self.name}: landing now.")
self.tower.notify_landed(self)
tower = ControlTower()
flight_a = Airplane("Flight A", tower)
flight_b = Airplane("Flight B", tower)
flight_a.request_landing() # Flight A: cleared to land.
flight_b.request_landing() # Flight B: hold — runway occupied.
Airplane objects never talk to each other directly — no plane holds a reference
to any other plane. Every coordination decision (who lands, who holds) is centralized in
ControlTower, which is exactly what keeps planes from needing to know anything about
each other's existence.
👍 When To Use It
- A set of objects need to react to each other's changes, and giving each one direct references to all the others would create a tangled many-to-many web.
- You want to centralize control logic that's currently scattered across several components, so it can be understood and changed from one place.
- Components should be reusable independently of each other — a mediator lets you swap or add components without rewiring every existing one.
👎 When NOT To Use It
- When there are only two or three components with a simple, stable relationship — direct references may be perfectly readable and a mediator would just add indirection.
- When the mediator itself starts absorbing so much logic that it becomes an unmanageable "god object" — centralizing coordination is only a win if the mediator stays focused and doesn't secretly reimplement every component's internal behavior.
- When components genuinely don't need to know about each other at all — if nothing needs to react to anything else, there's no coordination problem for a mediator to solve.
❓ FAQ
How is Mediator different from Observer?
Observer is a one-to-many broadcast: one subject notifies a list of observers, and communication flows one direction (subject → observers), with the subject not caring what the observers do in response. Mediator centralizes many-to-many coordination between a set of peer objects that would otherwise need direct references to each other — components notify the mediator of their own changes, and the mediator actively decides how multiple other components should react, rather than just broadcasting an event outward. In practice a Mediator is often implemented using Observer-style notifications internally, but the intent is different: Observer is about "announcing a change," Mediator is about "coordinating a conversation" between several objects.
Isn't the mediator just a new dependency everyone shares — haven't we just moved the coupling?
Fair concern, and the answer is: yes, coupling still exists, but it's reshaped into something far more manageable. Instead of an N×N web where every component potentially knows about every other component, you get N components each coupled to exactly one thing — the mediator. That's a much smaller, more centralized surface to understand, test, and change than a tangle of direct cross-references.
Can a system have more than one mediator?
Yes — large systems often split coordination into several mediators, each responsible for one cohesive group of components (one mediator per dialog, one per screen, one per subsystem), rather than a single mediator trying to coordinate the entire application. This keeps each mediator focused and avoids the "god object" trap mentioned above.
🙋 There Are No Dumb Questions
Q: Doesn't the mediator just become a giant switch statement of "if sender is X and
event is Y"?
A: It can, if a system's coordination logic is inherently complex — but that's still an
improvement over the same complexity being smeared across every component's source file. If a
single mediator's logic grows too large, the usual fix is splitting it into several smaller,
more focused mediators (see the FAQ above), not abandoning the pattern.
✅ Quick Check
In the Mediator pattern, how do components typically communicate with each other?
Next up: what if you need to save an object's internal state so it can be restored later — like an undo history — without breaking that object's encapsulation by exposing its private details to whoever's doing the saving? That's the Memento pattern.