Mediator
Reduce direct dependencies between objects by routing all communication through a central mediator. Objects no longer talk to each other directly — they talk to the mediator, which decides what to do.
The Problem
You have a chat room UI: a list of users, a message input, a send button, and a message history panel. When a user is selected, the input gets enabled. When text is typed, the send button activates. When send is clicked, the message goes to history. Every component directly references every other component — changing one breaks three others. This is a “spaghetti” dependency graph.
UserList ◄──► MessageInput ◄──► SendButton
▲ │ ▲
└───────────────►│◄──────────────┘
History
The Solution
Introduce a Mediator. Components communicate only with the mediator; they don’t reference each other. The mediator knows about all components and coordinates their interactions.
Real-World Analogy
Air traffic control at an airport. Planes don’t talk to each other directly — they’d create chaos. Each plane talks only to the control tower (mediator). The tower coordinates who lands, who holds, who takes off. Remove the tower and you need every plane to communicate with every other plane — O(n²) communication links.
TypeScript Example
// Mediator interface
interface FormMediator {
notify(sender: FormComponent, event: string, data?: unknown): void;
}
// Component base
abstract class FormComponent {
constructor(public mediator: FormMediator) {}
protected emit(event: string, data?: unknown): void {
this.mediator.notify(this, event, data);
}
}
// Concrete components — they ONLY know about the mediator
class UserSelector extends FormComponent {
private selectedUser: string | null = null;
selectUser(user: string): void {
this.selectedUser = user;
console.log(`[UserSelector] Selected: ${user}`);
this.emit('user:selected', user);
}
getSelectedUser(): string | null { return this.selectedUser; }
}
class MessageInput extends FormComponent {
private value = '';
private enabled = false;
enable(): void { this.enabled = true; console.log('[Input] Enabled'); }
disable(): void { this.enabled = false; }
setValue(v: string): void {
if (!this.enabled) return;
this.value = v;
this.emit('input:changed', v);
}
getValue(): string { return this.value; }
clear(): void { this.value = ''; }
}
class SendButton extends FormComponent {
private enabled = false;
enable(): void { this.enabled = true; console.log('[SendBtn] Enabled'); }
disable(): void { this.enabled = false; console.log('[SendBtn] Disabled'); }
click(): void {
if (!this.enabled) { console.log('[SendBtn] Ignored — not enabled'); return; }
console.log('[SendBtn] Clicked');
this.emit('button:send');
}
}
class MessageHistory extends FormComponent {
private messages: string[] = [];
addMessage(user: string, text: string): void {
const msg = `${user}: ${text}`;
this.messages.push(msg);
console.log(`[History] + "${msg}"`);
}
getAll(): string[] { return [...this.messages]; }
}
// Concrete Mediator — knows all components and wires them together
class ChatFormMediator implements FormMediator {
constructor(
private readonly userSelector: UserSelector,
private readonly input: MessageInput,
private readonly sendBtn: SendButton,
private readonly history: MessageHistory,
) {}
notify(sender: FormComponent, event: string, data?: unknown): void {
switch (event) {
case 'user:selected':
// Selecting a user enables the input
this.input.enable();
this.sendBtn.disable();
break;
case 'input:changed':
// Non-empty input enables the send button
const text = data as string;
if (text.length > 0) {
this.sendBtn.enable();
} else {
this.sendBtn.disable();
}
break;
case 'button:send':
// Send message: read from input + selector, write to history, clear input
const user = this.userSelector.getSelectedUser();
const msg = this.input.getValue();
if (user && msg) {
this.history.addMessage(user, msg);
this.input.clear();
this.sendBtn.disable();
}
break;
}
}
}
// Wire it up — use a factory so the mediator is wired before any component needs it
function createChatForm() {
// Placeholder mediator satisfies the constructor; replaced before any event fires
const placeholder: FormMediator = { notify() {} };
const userSelector = new UserSelector(placeholder);
const input = new MessageInput(placeholder);
const sendBtn = new SendButton(placeholder);
const history = new MessageHistory(placeholder);
const mediator = new ChatFormMediator(userSelector, input, sendBtn, history);
// Swap the real mediator in — all components share the same reference
userSelector.mediator = mediator;
input.mediator = mediator;
sendBtn.mediator = mediator;
history.mediator = mediator;
return { userSelector, input, sendBtn, history };
}
const { userSelector, input, sendBtn, history } = createChatForm();
// Simulate user interaction
console.log('--- Select user ---');
userSelector.selectUser('Alice');
console.log('\n--- Type message ---');
input.setValue('Hello World!');
console.log('\n--- Send ---');
sendBtn.click();
console.log('\n--- History ---');
console.log(history.getAll());
// → ['Alice: Hello World!']
When to Use
- Many objects communicate in complex, hard-to-understand ways (spaghetti dependencies)
- You can’t reuse a component because it’s too tightly coupled to others
- You want to customise behaviour (the coordination logic) without subclassing all components
- Examples: chat/messaging systems, UI form coordination, air traffic control, event buses, Redux reducers
Pros
- Single Responsibility — coordination logic is centralised in the mediator
- Open/Closed — add new components without changing existing ones
- Components become reusable because they don’t know about each other
- Reduces communication from O(n²) direct links to O(n) mediator links
Cons
- The mediator can become a “god object” as more coordination logic accumulates
- Can make the mediator itself complex and hard to reason about
Related Patterns
- Observer — components can publish events to the mediator (mediator is the subscriber)
- Facade simplifies a subsystem API; Mediator coordinates peer components
- Command can be used to encapsulate component requests sent to the mediator