Design patternBehavioral Patterns
Mediator
Route interactions between a group of components through one coordinator so they stop referencing each other and the rules live in one place.
- Complexity
- Medium
- Seen in the wild
- Medium
- Also known as
- Intermediary, Controller
Intent
Stop a set of components from calling each other directly. Each component reports what happened to one mediator object, and the mediator decides which other components to update. The coordination rules move out of the components and into a single place you can read and test.
Forces
- Components that must react to each other vs components you want to reuse alone: a room picker that calls the price label can't be used on a page without one.
- Rules spread across peers vs rules you can read in one place: "more guests filters rooms, and any change reprices" ends up split over four event handlers.
- N components vs N×N links: every new component adds wiring to every existing one.
- One central coordinator vs that coordinator growing without bound: the cure for tangled peers can become a god object.
Therefore
Give the components a reference only to a mediator interface, have them report changes to it, and put every cross-component rule in the mediator.
Components (often called colleagues) stay simple: they hold their own state, expose methods to update it, and call mediator.changed(event) when the user acts. Methods the mediator calls on a component do not notify back, which avoids feedback loops. The mediator is specific to one workflow. When it starts to serve several unrelated workflows, split it.
Structure
Diagram source (Mermaid)
classDiagram
class PanelMediator {
<<interface>>
+changed(event) void
}
class BookingPanel {
-Room[] catalog
+changed(event) void
}
class NumberInput {
-PanelMediator mediator
-int value
+set(value) void
}
class RoomSelect {
-PanelMediator mediator
-Room selected
+choose(code) void
+limitTo(rooms) void
}
class QuoteLabel {
-String text
+show(text) void
}
PanelMediator <|.. BookingPanel
BookingPanel *-- NumberInput
BookingPanel *-- RoomSelect
BookingPanel *-- QuoteLabel
NumberInput --> PanelMediator
RoomSelect --> PanelMediatorParticipants
- Mediator (
PanelMediator): the narrow interface components use to report events. Components never see more than this. - Concrete mediator (
BookingPanel): owns the components and holds every rule about how they affect each other. - Colleagues (
NumberInput,RoomSelect,QuoteLabel): hold their own state and notify the mediator. They do not reference each other. - Client: creates the mediator and talks to the colleagues through it, or lets user input drive them.
Use it when
- A group of UI widgets, services or devices has many-to-many update rules, and changing one rule means editing several components.
- You want to reuse a component in another screen or flow, but it references its current neighbors directly.
- The interaction rules are the hard part of the feature and need their own tests.
- Adding a component today means touching every existing component. See Shotgun Surgery.
Do not use it when
- One component updates one other, in one direction. A direct call or a callback is clearer.
- Components only need to announce events and listeners don't coordinate with each other. Use Observer or an event emitter.
- The coordination is one-way data flow: one owner of state, widgets that only derive from it. Lift the state to a parent, or use a store (Redux, Pinia, a Svelte store) or a view model, and derive each widget's props from that state. A mediator earns its name only when peers talk back and the coordination has rules of its own.
- The mediator would coordinate unrelated workflows. Several small mediators beat one god object that every feature edits. If yours passes a few hundred lines or has a
switchover dozens of events, split it by workflow. - The "mediator" only forwards calls to a subsystem without coordinating peers. That is a Facade.
Application grill
Inspect first: the component tangle the pattern would untie (who calls whom for one workflow); existing event mechanisms (an event emitter, signals, streams, a message broker) - a mediator over an existing bus may be configuration, not code.
Missing pieces: in-process: the language's event emitter or a small coordinator object - nothing to buy. Cross-service: a broker (Redis pub/sub for transient fan-out, Kafka or SQS for durability and ordering), which brings retention and ordering semantics to decide - do not add one for a single coordinator.
Ask before you build:
- Scale: how many components does the mediator join - three is an object, fifteen is an architecture?
- Performance: is it on the request path (synchronous) or the event path (asynchronous, with latency tolerance)?
- Reliability: when one participant fails, who notices - what is the mediator's error policy?
- Budget: is a broker justified for one interaction, or is in-process enough until a second service needs the events?
- Implementation: are interactions typed (request and response through the mediator) or fire-and-forget?
Implementation steps
- List the components and draw every place one references another. Each arrow is a rule to move.
- Define a mediator interface with one notification method, for example
changed(event). Keep it this narrow. - Replace each component's references to its peers with a reference to the mediator interface.
- Move every cross-component rule into the concrete mediator. Methods the mediator calls on components must not notify back.
- Have the mediator create or receive the components and set the initial state in one place.
- Test the mediator's rules directly: change one component and assert the state of the others.
Example
A hotel booking panel has a guest count, a night count, a room picker and a price quote. More guests than a room holds removes that room from the picker and selects one that fits. Any change reprices the quote. None of the widgets knows the others exist.
interface Room { code: string; capacity: number; nightlyCents: number }
type PanelEvent = "guests" | "nights" | "room";
interface PanelMediator {
changed(event: PanelEvent): void;
}
class NumberInput {
constructor(private readonly mediator: PanelMediator, private readonly event: PanelEvent, public value: number) {}
set(value: number): void {
this.value = value;
this.mediator.changed(this.event);
}
}
class RoomSelect {
options: Room[] = [];
selected?: Room;
constructor(private readonly mediator: PanelMediator) {}
choose(code: string): void {
this.selected = this.options.find((r) => r.code === code) ?? this.selected;
this.mediator.changed("room");
}
// Called by the mediator. It does not notify back, which prevents loops.
limitTo(rooms: Room[]): void {
this.options = rooms;
if (!this.selected || !rooms.includes(this.selected)) this.selected = rooms[0];
}
}
class QuoteLabel {
text = "";
show(text: string): void { this.text = text; }
}
class BookingPanel implements PanelMediator {
readonly guests = new NumberInput(this, "guests", 2);
readonly nights = new NumberInput(this, "nights", 1);
readonly rooms = new RoomSelect(this);
readonly quote = new QuoteLabel();
constructor(private readonly catalog: Room[]) {
this.changed("guests");
}
// Every cross-widget rule lives here.
changed(event: PanelEvent): void {
if (event === "guests") {
this.rooms.limitTo(this.catalog.filter((r) => r.capacity >= this.guests.value));
}
const room = this.rooms.selected;
const nights = this.nights.value;
this.quote.show(room ? `${room.code} x${nights}: $${((room.nightlyCents * nights) / 100).toFixed(2)}` : "No room fits this party");
}
}
const panel = new BookingPanel([
{ code: "STD", capacity: 2, nightlyCents: 8900 },
{ code: "DBL", capacity: 2, nightlyCents: 11900 },
{ code: "FAM", capacity: 4, nightlyCents: 18900 },
]);
panel.rooms.choose("DBL");
panel.nights.set(3);
console.log(panel.quote.text); // DBL x3: $357.00
panel.guests.set(4);
console.log(panel.quote.text); // FAM x3: $567.00
panel.guests.set(6);
console.log(panel.quote.text); // No room fits this partyTrade-offs
Gains
- Components are reusable: they depend on a one-method interface, not on their neighbors.
- Interaction rules live in one class you can read top to bottom and test directly.
- N×N links become N links to the mediator.
- Adding a component touches the mediator only.
Costs
- The mediator tends to absorb every rule and becomes a Large Class.
- Control flow is indirect: to see what a click does, you read the mediator, not the widget.
- Notifications that trigger notifications can loop unless the mediator's own calls are silent.
- For two or three components, it is more structure than the problem has.
Relations
- Observer is often how colleagues notify the mediator. Observer broadcasts to anyone; Mediator encodes who should react and how.
- Facade simplifies access to a subsystem from outside, and the subsystem doesn't know about it. A mediator is known to its colleagues and coordinates them.
- Chain of Responsibility spreads a decision across handlers. Mediator concentrates it in one object.
- Command objects can be what colleagues send to the mediator, which makes the interactions loggable and undoable.
Agent checklist
- At least three components have cross-dependencies today. Otherwise keep direct calls or a callback.
- If a framework store or parent component already owns the state, you derived props from it instead of adding a mediator.
- Colleagues reference only the mediator interface, never each other or the concrete mediator.
- Methods the mediator calls on colleagues do not notify back.
- The mediator serves one workflow. Tests cover each rule by changing one colleague and asserting the others.
See also
- ObserverLet an object announce its changes to a list of subscribers it does not know, so new reactions plug in without editing the source of the change.
- FacadeGive a complex subsystem one small entry point for a common task, so callers stop orchestrating its classes and calling them in the right order.
- Chain of ResponsibilityPass a request along a chain of handlers until one decides it, so the sender never knows which handler acts or how many exist.
- CommandWrap an action and its arguments in an object so you can queue, log, retry or undo it separately from the code that triggers it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.