Behavioral Pattern (GoF)

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:

  1. 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.
  2. 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.
  3. 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)

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

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.