designpattern.site

Mediator Pattern

Define an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly.

Overview

When multiple objects need to communicate with each other, the most direct approach is to wire them together directly. Each object holds references to the others and calls their methods. This works fine for two or three objects — but as the number of participants grows, the number of direct connections grows quadratically. A system of ten objects can have up to 45 direct links, and changing any one object risks breaking all the objects connected to it.

The Mediator pattern is a behavioral design pattern that solves this by introducing a central hub — the mediator — that all objects talk to instead of talking to each other. Objects no longer need to know about one another; they only need to know about the mediator. This reduces a many-to-many communication mesh to a set of one-to-many spokes.

Think of an air traffic control tower. Aircraft do not communicate directly with each other to negotiate landing order and runway assignments. Instead, every aircraft reports to the tower, and the tower coordinates all decisions. The aircraft stay simple; the coordination complexity lives in one place.

Problem & Motivation

Imagine building a chat room application. You have a User class and you want users to be able to send messages to each other. The naive design gives each User a list of references to every other User, and calling sendMessage() iterates that list.

This creates several concrete problems:

  • Coupling explodes — every User object depends directly on every other User. Adding a new type of participant (a bot, a moderator, a system announcement) requires changing the User class.
  • Reuse is impossible — you cannot use a User in a different context (a private message system, a game lobby) without dragging along all the other users it references.
  • Logic is scattered — rules like "mute this user" or "broadcast a join notification" live partly in every User rather than in one authoritative place.

The Mediator pattern extracts all coordination logic into a ChatRoom object. Users register with the chat room and send messages to it. The chat room decides who receives what. Users know only about the chat room interface, not about each other.

Class Diagram

Loading diagram...

The structural roles are:

  • Mediator (interface) — declares the notify method that components call to trigger coordination.
  • ConcreteMediator (ChatRoom) — implements coordination logic. It holds references to all components and routes events between them.
  • Component (base class) — knows only about the Mediator interface. It calls notify() but has no knowledge of other components.
  • ConcreteComponent (User) — a participant in the system. It delegates all cross-component communication to the mediator.

Implementation

The implementations below model a ChatRoom that coordinates message broadcasting between User participants. Adding or removing a user, or changing message routing rules, only touches the ChatRoom — not the User class.

interface Mediator {
  notify(sender: User, event: string, data?: string): void;
}

class User {
  private mediator: Mediator | null = null;

  constructor(public readonly name: string) {}

  setMediator(mediator: Mediator): void {
    this.mediator = mediator;
  }

  send(message: string): void {
    console.log(`${this.name} sends: "${message}"`);
    this.mediator?.notify(this, "message", message);
  }

  receive(from: string, message: string): void {
    console.log(`${this.name} receives from ${from}: "${message}"`);
  }
}

class ChatRoom implements Mediator {
  private users: User[] = [];

  addUser(user: User): void {
    this.users.push(user);
    user.setMediator(this);
  }

  notify(sender: User, event: string, data?: string): void {
    if (event === "message" && data !== undefined) {
      for (const user of this.users) {
        if (user !== sender) {
          user.receive(sender.name, data);
        }
      }
    }
  }
}

// Usage
const room = new ChatRoom();
const alice = new User("Alice");
const bob = new User("Bob");
const carol = new User("Carol");

room.addUser(alice);
room.addUser(bob);
room.addUser(carol);

alice.send("Hello, everyone!");
// Bob receives from Alice: "Hello, everyone!"
// Carol receives from Alice: "Hello, everyone!"

bob.send("Hey Alice!");
// Alice receives from Bob: "Hey Alice!"
// Carol receives from Bob: "Hey Alice!"

The Mediator Is Not a God Object

A common mistake is letting the mediator accumulate every piece of business logic until it becomes an unmaintainable monolith. The mediator's job is coordination — routing events and triggering responses — not implementing behavior that belongs in the components themselves. If your mediator is thousands of lines long, consider splitting it into smaller mediators, each responsible for a coherent subsystem.

Real-World Examples

GUI dialog boxes — A form with a checkbox that enables or disables other fields is a classic mediator scenario. The dialog box acts as the mediator: when the checkbox state changes, the dialog re-evaluates which fields should be active, rather than having the checkbox know about every field it controls. This is how most UI frameworks wire complex forms internally.

Air traffic control systems — Aircraft approaching an airport do not negotiate directly with each other to determine landing sequence and runway assignments. Every aircraft communicates with the control tower, which holds the global state (which runways are occupied, what the weather is, who has priority) and issues clearances. Adding a new type of aircraft or a new runway rule only changes the tower's logic.

Event broker / message bus — Frameworks like Redux, NgRx, and MobX implement a Mediator-style store. UI components dispatch actions to the store (the mediator) and subscribe to state slices. Components never communicate directly with each other; the store coordinates all state transitions. The same pattern appears in backend message brokers like RabbitMQ and Apache Kafka, where producers and consumers are decoupled through the broker.

Pros and Cons

Advantages

Loose coupling — components hold no references to each other, only to the mediator interface. You can add, remove, or replace a component without touching the others.

Centralized control flow — all interaction logic lives in one place, making the system easier to understand, debug, and audit.

Reusable components — because a User knows nothing about other User objects, you can reuse the same class in a different context simply by pairing it with a different mediator.

Easier to extend — adding a new type of event or a new routing rule means modifying only the mediator, not every component.

Disadvantages

Mediator can become a bottleneck — every interaction passes through a single object. In high-throughput systems this can create a performance or concurrency hotspot.

Risk of a god object — without discipline, the mediator absorbs more and more logic until it is the hardest class to understand and change in the entire codebase.

Indirection hides flow — calling mediator.notify(this, "message", data) is less obvious than a direct method call. Tracing a message from sender to receiver requires understanding the mediator's routing logic.

Testability tradeoff — while components become more testable in isolation, the mediator itself must be tested against all the interaction scenarios it handles, which can be a large surface area.

When to Use / When to Avoid

Use Mediator when:

  • Several objects communicate in complex, tightly coupled ways that make the system hard to change.
  • You cannot reuse a component because it depends too heavily on other concrete components.
  • You find yourself creating many subclasses to customize behavior that is fundamentally about coordinating multiple objects.
  • You need one authoritative place to apply rules (rate limiting, muting, permission checks) that cross component boundaries.

Avoid Mediator when:

  • Only two or three objects interact — a simple direct reference is clearer and has no coordination overhead.
  • The interaction logic is trivial — introducing a mediator adds indirection without delivering meaningful decoupling.
  • Performance is critical and adding a coordination layer between all messages is unacceptable.
  • The components already have a clean interface contract and don't need a hub to orchestrate them.

Observer — Observer and Mediator both decouple senders from receivers, but in different ways. In Observer, components subscribe directly to each other's events. In Mediator, components subscribe to nothing; the mediator pushes updates. Use Observer when the relationship between two objects is direct and 1-to-many; use Mediator when the web of relationships is more complex and centralized control is desirable.

Facade — Facade provides a simplified interface to a complex subsystem but does not coordinate the subsystem's internal objects — they are still allowed to call each other. Mediator actively routes and coordinates communication between objects that would otherwise be coupled.

Command — Command encapsulates a request as an object, which can be useful for implementing undo/redo. A Mediator can use Command objects to represent the events that components send it, turning informal notify calls into structured, loggable operations.

Chain of Responsibility — in Chain of Responsibility, a request travels along a chain until one handler processes it. In Mediator, all routing decisions are made centrally. Choose Chain of Responsibility when the handler should be determined at runtime by the handlers themselves; choose Mediator when a single authority should make all routing decisions.

Design Tip

If you are building a UI with many interdependent controls, start by sketching the interactions on paper and asking: "who is responsible for knowing about this dependency?" If the answer is always "both objects," that is a sign you need a mediator. The dialog or screen itself is the natural candidate — it already has references to all its controls and is a fitting home for coordination logic.

On this page