Mediator
Centralizes communication between objects in a mediator, eliminating direct references between them — reducing coupling from M:N (every object knows every other) to M:1 (every object knows only the mediator).
Intent
Replace cross-references between objects with a central hub — the Mediator — that knows all participants (colleagues) and coordinates communication between them. Each colleague sends its notifications to the Mediator and receives instructions from it; colleagues don't know the others exist.
Cataloged by the GoF (1994) as a behavioral pattern, Mediator shows up in complex UI forms (fields that enable each other), chat rooms, air-traffic control systems, processing pipelines, and any context where N objects would need to reference each other — creating a dependency graph that's hard to maintain.
Problem
Imagine a signup form with four components: a "Company" checkbox, a tax-ID field, a personal-ID field, and a Submit button. The interaction rules are: checking "Company" enables the tax-ID field and disables the personal-ID field; the Submit button only becomes active when the relevant field is filled in. The direct approach couples every component to all the others:
// Naive approach — DON'T do this:
class CompanyCheckbox {
constructor(
private taxIdField: TaxIdField, // direct reference
private personalIdField: PersonalIdField, // direct reference
private submitButton: SubmitButton // direct reference
) {}
toggle(checked: boolean): void {
if (checked) {
this.taxIdField.enable();
this.personalIdField.disable();
} else {
this.taxIdField.disable();
this.personalIdField.enable();
}
this.submitButton.update(checked);
// Every new component requires modifying this class.
}
}
With N components, each needs to know the other N-1. Coupling grows quadratically (M:N). Tests become hard because isolating one component requires mocking all its peers. Adding a new component to the form requires modifying every existing component that must react to it.
Mediator solves this: each component only knows the Mediator and sends notifications to it. The logic of "who reacts to what" is centralized in a single place — the ConcreteMediator.
Solution
Mediator organizes the code into three participants:
-
Mediator (interface): declares the communication
method colleagues use to notify the mediator. E.g.:
notify(sender, event). Colleagues only know this interface — not the specific ConcreteMediator. - ConcreteMediator: knows every concrete colleague and implements the coordination logic. Upon receiving a notification, it decides which colleagues should be triggered and how. It's the only place where the interaction logic between components exists.
- Colleagues: the components that interact with each other only through the Mediator. Each colleague keeps a reference to the Mediator (injected via constructor or setter) and notifies it when its state changes. It never calls other colleagues directly.
Resolving the circular reference
The Mediator needs to know the colleagues, and the colleagues need
to know the Mediator — creating a circular dependency at
construction time. The idiomatic solution is setter injection: build
the colleagues first, build the Mediator passing the colleagues in,
then inject the Mediator into the colleagues with
setMediator(). This way, no constructor needs to
receive an object that doesn't exist yet.
Structure
«interface»
Mediator
┌──────────────────────────────────────┐
│ + notify(sender: Component, │
│ event: string): void │
└──────────────────────────────────────┘
▲
FormMediator (ConcreteMediator)
┌──────────────────────────────────────┐
│ - checkbox: TermsCheckbox │
│ - field: CodeField │
│ - button: SubmitButton │
│ + notify(sender, event): void │
│ → applies coordination rules │
└──────────────────────────────────────┘
│ coordinates
┌────────┼────────────┐
▼ ▼ ▼
TermsCheckbox CodeField SubmitButton
(Colleague) (Colleague) (Colleague)
│ │
└──────┬────────┘
│ notifies via
▼
Mediator
Communication flow:
checkbox.accept()
→ this.mediator.notify(this, "terms:accepted")
→ FormMediator checks the state of all colleagues
→ button.enable() (if the field is also filled in)
→ button.disable() (otherwise)
Code examples
Example 1 — Chat room with decoupled participants
The classic example: multiple participants communicate through a
room (the Mediator). No participant references the others directly
— they send messages to the room and receive notifications from it.
The logic of "who receives what" lives entirely in the
ChatRoom, not in the participants.
// ── Mediator interface ────────────────────────────────────────
interface ChatMediator {
sendMessage(message: string, sender: Participant): void;
}
// ── Colleague ─────────────────────────────────────────────────
// Participant doesn't know other participants — only the mediator.
class Participant {
constructor(
readonly name: string,
private readonly mediator: ChatMediator
) {}
send(message: string): void {
console.log(`${this.name} → room: "${message}"`);
this.mediator.sendMessage(message, this);
}
receive(message: string, sender: Participant): void {
console.log(` [${this.name} ← ${sender.name}]: "${message}"`);
}
}
// ── ConcreteMediator ──────────────────────────────────────────
class ChatRoom implements ChatMediator {
private readonly participants: Set<Participant> = new Set();
join(p: Participant): void { this.participants.add(p); }
leave(p: Participant): void { this.participants.delete(p); }
sendMessage(message: string, sender: Participant): void {
// Delivers to everyone except the sender — logic centralized here.
for (const p of this.participants) {
if (p !== sender) {
p.receive(message, sender);
}
}
}
}
// ── Usage ────────────────────────────────────────────────────
const room = new ChatRoom();
const alice = new Participant('Alice', room);
const bob = new Participant('Bob', room);
const carol = new Participant('Carol', room);
room.join(alice);
room.join(bob);
room.join(carol);
alice.send('Good morning everyone!');
// Alice → room: "Good morning everyone!"
// [Bob ← Alice]: "Good morning everyone!"
// [Carol ← Alice]: "Good morning everyone!"
room.leave(carol);
bob.send('Carol has left the room.');
// Bob → room: "Carol has left the room."
// [Alice ← Bob]: "Carol has left the room."
// (Carol doesn't receive it — already left)
<?php
// ── Mediator interface ────────────────────────────────────────
interface ChatMediator
{
public function sendMessage(string $message, Participant $sender): void;
}
// ── Colleague ─────────────────────────────────────────────────
class Participant
{
public function __construct(
public readonly string $name,
private readonly ChatMediator $mediator
) {}
public function send(string $message): void
{
echo "{$this->name} → room: \"{$message}\"\n";
$this->mediator->sendMessage($message, $this);
}
public function receive(string $message, Participant $sender): void
{
echo " [{$this->name} ← {$sender->name}]: \"{$message}\"\n";
}
}
// ── ConcreteMediator ──────────────────────────────────────────
class ChatRoom implements ChatMediator
{
/** @var Participant[] */
private array $participants = [];
public function join(Participant $p): void
{
$this->participants[] = $p;
}
public function leave(Participant $p): void
{
$this->participants = array_values(array_filter(
$this->participants,
fn($x) => $x !== $p
));
}
public function sendMessage(string $message, Participant $sender): void
{
foreach ($this->participants as $p) {
if ($p !== $sender) {
$p->receive($message, $sender);
}
}
}
}
// ── Usage ────────────────────────────────────────────────────
$room = new ChatRoom();
$alice = new Participant('Alice', $room);
$bob = new Participant('Bob', $room);
$carol = new Participant('Carol', $room);
$room->join($alice);
$room->join($bob);
$room->join($carol);
$alice->send('Good morning everyone!');
// Alice → room: "Good morning everyone!"
// [Bob ← Alice]: "Good morning everyone!"
// [Carol ← Alice]: "Good morning everyone!"
$room->leave($carol);
$bob->send('Carol has left the room.');
// Bob → room: "Carol has left the room."
// [Alice ← Bob]: "Carol has left the room."
Example 2 — Form with interdependent components
UI components that need to react to each other are the most common
use case for Mediator in real applications. In this example, a
checkbox and a code field control the state of a Submit button — all
the coordination logic lives in FormMediator, and the
components simply notify it when their state changes.
// ── Mediator interface ────────────────────────────────────────
interface Mediator {
notify(sender: Component, event: string): void;
}
// ── Base class for colleagues ─────────────────────────────────
class Component {
private mediator: Mediator | null = null;
// Setter avoids a circular reference in the constructor.
setMediator(m: Mediator): void { this.mediator = m; }
protected notifyMediator(event: string): void {
this.mediator?.notify(this, event);
}
}
// ── Concrete colleagues ────────────────────────────────────────
class TermsCheckbox extends Component {
private accepted = false;
accept(): void {
this.accepted = true;
console.log('Checkbox: terms accepted');
this.notifyMediator('terms:accepted');
}
isAccepted(): boolean { return this.accepted; }
}
class CodeField extends Component {
private value = '';
fill(code: string): void {
this.value = code;
console.log(`Code field: "${code}"`);
this.notifyMediator('code:filled');
}
getValue(): string { return this.value; }
}
class SubmitButton extends Component {
enable(): void { console.log('Submit button: enabled'); }
disable(): void { console.log('Submit button: disabled'); }
}
// ── ConcreteMediator ──────────────────────────────────────────
// All the coordination logic lives here — colleagues don't know each other.
class FormMediator implements Mediator {
constructor(
private readonly checkbox: TermsCheckbox,
private readonly field: CodeField,
private readonly button: SubmitButton
) {}
notify(_sender: Component, event: string): void {
const termsOk = this.checkbox.isAccepted();
const codeOk = this.field.getValue().length >= 4;
console.log(
` [Mediator] event="${event}" → terms=${termsOk} code=${codeOk}`
);
if (termsOk && codeOk) {
this.button.enable();
} else {
this.button.disable();
}
}
}
// ── Usage ────────────────────────────────────────────────────
const checkbox = new TermsCheckbox();
const field = new CodeField();
const button = new SubmitButton();
// Mediator created after the colleagues; then injected into them via setter.
const mediator = new FormMediator(checkbox, field, button);
checkbox.setMediator(mediator);
field.setMediator(mediator);
button.setMediator(mediator);
checkbox.accept();
// Checkbox: terms accepted
// [Mediator] event="terms:accepted" → terms=true code=false
// Submit button: disabled
field.fill('AB12');
// Code field: "AB12"
// [Mediator] event="code:filled" → terms=true code=true
// Submit button: enabled
<?php
// ── Mediator interface ────────────────────────────────────────
interface Mediator
{
public function notify(Component $sender, string $event): void;
}
// ── Base class for colleagues ─────────────────────────────────
abstract class Component
{
private ?Mediator $mediator = null;
public function setMediator(Mediator $m): void
{
$this->mediator = $m;
}
protected function notifyMediator(string $event): void
{
$this->mediator?->notify($this, $event);
}
}
// ── Concrete colleagues ────────────────────────────────────────
class TermsCheckbox extends Component
{
private bool $accepted = false;
public function accept(): void
{
$this->accepted = true;
echo "Checkbox: terms accepted\n";
$this->notifyMediator('terms:accepted');
}
public function isAccepted(): bool { return $this->accepted; }
}
class CodeField extends Component
{
private string $value = '';
public function fill(string $code): void
{
$this->value = $code;
echo "Code field: \"{$code}\"\n";
$this->notifyMediator('code:filled');
}
public function getValue(): string { return $this->value; }
}
class SubmitButton extends Component
{
public function enable(): void { echo "Submit button: enabled\n"; }
public function disable(): void { echo "Submit button: disabled\n"; }
}
// ── ConcreteMediator ──────────────────────────────────────────
class FormMediator implements Mediator
{
public function __construct(
private readonly TermsCheckbox $checkbox,
private readonly CodeField $field,
private readonly SubmitButton $button
) {}
public function notify(Component $sender, string $event): void
{
$termsOk = $this->checkbox->isAccepted();
$codeOk = strlen($this->field->getValue()) >= 4;
echo " [Mediator] event=\"{$event}\"" .
" → terms=" . ($termsOk ? 'true' : 'false') .
" code=" . ($codeOk ? 'true' : 'false') . "\n";
if ($termsOk && $codeOk) {
$this->button->enable();
} else {
$this->button->disable();
}
}
}
// ── Usage ────────────────────────────────────────────────────
$checkbox = new TermsCheckbox();
$field = new CodeField();
$button = new SubmitButton();
$mediator = new FormMediator($checkbox, $field, $button);
$checkbox->setMediator($mediator);
$field->setMediator($mediator);
$button->setMediator($mediator);
$checkbox->accept();
// Checkbox: terms accepted
// [Mediator] event="terms:accepted" → terms=true code=false
// Submit button: disabled
$field->fill('AB12');
// Code field: "AB12"
// [Mediator] event="code:filled" → terms=true code=true
// Submit button: enabled
When to use
- When many objects reference each other and the dependency graph makes maintenance, testing, and comprehension harder — a clear sign that M:N needs to become M:1.
- In complex UI forms and dialogs where fields enable, disable, or populate each other depending on the state of other fields — the Mediator is the natural place to centralize these interaction rules.
- In communication systems between agents (chat rooms, conference rooms, dispatch systems) where participants shouldn't know each other directly, to make joining, leaving, and replacement easier.
- To make testing easier: mocking the Mediator lets you test each component in isolation without needing to assemble the full set of colleagues.
When to avoid
- For simple communication between two or three objects: if the coordination is trivial and stable, the Mediator abstraction adds complexity with no noticeable benefit.
- When the Mediator starts accumulating business logic: if every domain rule migrates into the Mediator, it becomes a disguised god object. When that happens, it's a sign that the responsibility should be split or that the domain needs to be modeled differently.
- When flow traceability is critical: centralizing all communication in a single point can make understanding complex flows harder — "who notified whom and when" requires inspecting the entire Mediator. In systems with many events, consider tracing tools or specific logging in the Mediator.
Pros and cons
Pros
- Reduces coupling from M:N to M:1 — each component depends only on the Mediator, not on the others.
- Centralizes coordination logic in a single place — easier to audit, modify, and debug the interaction rules.
- Colleagues can be added or removed without impacting the other colleagues — only the Mediator needs to be updated.
- Makes unit testing easier — a mock of the Mediator fully isolates the component being tested.
Cons
- The Mediator can grow uncontrollably and turn into a god object if every new coordination rule is centralized in it without criteria.
- All communication flows through a single point — in systems with many events, the Mediator can become a performance or comprehension bottleneck.
- The Mediator needs to know every concrete colleague, creating bidirectional coupling between it and each component.
Common pitfalls
1. Mediator turns into a god object
The most frequent pitfall: as the system grows, every new coordination rule is added to the Mediator. It quickly accumulates logic from multiple domains and becomes a huge class nobody wants to touch. The warning sign is when the Mediator has more logic than the colleagues combined.
Solution: when the Mediator grows too much, decompose it into smaller Mediators by responsibility, or revisit the design — maybe some colleagues should have more internal logic and notify the Mediator less.
2. Confusing Mediator with Observer
Observer defines a one-to-many dependency: the Subject publishes an event and N Observers react independently — the Subject doesn't know the Observers' concrete types, only their interface. Communication is unidirectional and decoupled: the Subject doesn't know who reacted.
Mediator defines bidirectional coordination: the Mediator explicitly knows each concrete colleague and decides, based on the state of multiple colleagues, which actions to take. Communication flows both ways — colleagues notify the Mediator, and the Mediator triggers colleagues. Use Observer when the pattern is "unknown broadcaster → multiple decoupled listeners"; use Mediator when the pattern is "hub that coordinates flow between known participants".
3. Circular reference at construction time
The Mediator needs references to the colleagues, and the colleagues
need a reference to the Mediator — neither can be created first
without the other. The standard solution is setter injection:
create the colleagues first (with the Mediator as null
or a stub), build the Mediator passing the colleagues in, then
inject the Mediator into the colleagues with
setMediator(). Another approach is injecting the
Mediator via the colleagues' constructor and creating the Mediator
in a second step after initializing the colleagues.
4. Mediator vs Facade — a distinction of direction
Facade provides a simplified interface to a subsystem from an external point of view: a client calls the Facade, which coordinates the subsystem internally. Communication is unidirectional (client → Facade → subsystem). The Facade doesn't know the client; the subsystem doesn't know the Facade back.
Mediator coordinates internal communication between components that already exist in the system: colleagues notify the Mediator and receive notifications from it — bidirectional. The Mediator knows the colleagues and they know the Mediator. These are complementary patterns, not alternatives.
Related patterns
Mediator relates to communication and coordination patterns:
Observer and Mediator are the two most-often-confused object-communication patterns. Observer = decoupled 1→N broadcast per event (the Subject doesn't know the Observers individually); Mediator = a hub that knows the colleagues and coordinates bidirectional flow between them. They're complementary: a Mediator can internally use Observer to notify colleagues about changes. Facade simplifies external access to a subsystem (unidirectional); Mediator coordinates internal interactions between components that communicate mutually (bidirectional) — a distinction of direction and of who knows whom. Command can be routed through a Mediator: instead of each component directly calling the receiver of an action, it creates a Command and hands it to the Mediator, which decides how and to whom to dispatch it.