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.

«interface» Mediator + notify(sender, event) ConcreteMediator + notify(sender, event) ComponentA ComponentB ComponentC ... coordinates all
Mediator — all components notify the Mediator; it coordinates responses without direct coupling

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

  • 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