StackPractices
intermediate By Mathias Paulenko

Mediator Pattern

Define an object that encapsulates how a set of objects interact. A behavioral design pattern for reducing chaotic dependencies.

Topics: design

Overview

The Mediator Pattern is a behavioral design pattern that defines an object that encapsulates how a set of objects interact. Instead of objects referring to each other directly, they communicate through a central mediator. This reduces the number of direct connections between components and centralizes complex coordination logic.

When to Use

Use the Mediator Pattern when:

  • You have many objects that need to communicate in complex ways
  • The dependencies between objects create a tangled mess (spaghetti code)
  • You want to centralize complex coordination logic
  • Reusing an individual component is hard because it depends on many others
  • A change in one component forces changes in many others

Solution

Python

from abc import ABC, abstractmethod

class ChatMediator(ABC):
    @abstractmethod
    def send_message(self, message: str, sender):
        pass

    @abstractmethod
    def add_user(self, user):
        pass

class ChatRoom(ChatMediator):
    def __init__(self):
        self.users = []

    def add_user(self, user):
        self.users.append(user)

    def send_message(self, message: str, sender):
        for user in self.users:
            if user != sender:
                user.receive(message, sender.name)

class User:
    def __init__(self, name: str, mediator: ChatMediator):
        self.name = name
        self.mediator = mediator
        mediator.add_user(self)

    def send(self, message: str):
        print(f"{self.name} sends: {message}")
        self.mediator.send_message(message, self)

    def receive(self, message: str, from_name: str):
        print(f"{self.name} receives from {from_name}: {message}")

# Usage
room = ChatRoom()
alice = User("Alice", room)
bob = User("Bob", room)

alice.send("Hello everyone!")

JavaScript

class ChatRoom {
  constructor() {
    this.users = [];
  }

  addUser(user) {
    this.users.push(user);
  }

  sendMessage(message, sender) {
    for (const user of this.users) {
      if (user !== sender) {
        user.receive(message, sender.name);
      }
    }
  }
}

class User {
  constructor(name, mediator) {
    this.name = name;
    this.mediator = mediator;
    mediator.addUser(this);
  }

  send(message) {
    console.log(`${this.name} sends: ${message}`);
    this.mediator.sendMessage(message, this);
  }

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

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

alice.send("Hello everyone!");

Java

import java.util.ArrayList;
import java.util.List;

public interface ChatMediator {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

public class ChatRoom implements ChatMediator {
    private final List<User> users = new ArrayList<>();

    public void addUser(User user) {
        users.add(user);
    }

    public void sendMessage(String message, User sender) {
        for (User user : users) {
            if (user != sender) {
                user.receive(message, sender.getName());
            }
        }
    }
}

public class User {
    private final String name;
    private final ChatMediator mediator;

    public User(String name, ChatMediator mediator) {
        this.name = name;
        this.mediator = mediator;
        mediator.addUser(this);
    }

    public String getName() { return name; }

    public void send(String message) {
        System.out.println(name + " sends: " + message);
        mediator.sendMessage(message, this);
    }

    public void receive(String message, String fromName) {
        System.out.println(name + " receives from " + fromName + ": " + message);
    }
}

// Usage
ChatRoom room = new ChatRoom();
User alice = new User("Alice", room);
User bob = new User("Bob", room);
alice.send("Hello everyone!");

Explanation

The Mediator Pattern has two roles:

  • Mediator (ChatRoom): Defines the interface for communication between components
  • Colleagues (User): Objects that communicate through the mediator instead of directly

Without the mediator, each user would need a reference to every other user. With it, each user only needs a reference to the mediator.

Variants

VariantDescriptionUse Case
Event BusDecoupled pub/sub via a central channelLarge systems with many publishers/subscribers
Command BusCommands routed through a central handlerCQRS, task dispatching
Dialog DirectorUI widgets coordinated by a dialog controllerForm validation, wizard flows

What Works

  • Keep the mediator focused on coordination, not business logic
  • Avoid turning the mediator into a god object — if it grows too large, split it
  • Document which events the mediator handles so colleagues know what to expect
  • Consider an event bus for very large systems where a single mediator would become a bottleneck
  • Make the mediator observable so external systems can monitor interactions

Common Mistakes

  • Putting too much logic into the mediator, creating a “god class” that is hard to maintain
  • Using a mediator when simple direct method calls would suffice (over-engineering)
  • Making the mediator a bottleneck by centralizing all communication in a synchronous blocking fashion
  • Not documenting the mediator’s role, making it hard to understand why components don’t communicate directly
  • Allowing the mediator to leak colleague details to other colleagues

Advanced Topics

Scenario: Mediator for Multi-Step Wizard Form

// Mediator: coordinate wizard steps without coupling
interface WizardMediator {
  notify(sender: string, event: string, data?: unknown): void;
}

abstract class WizardStep {
  constructor(protected mediator: WizardMediator, public name: string) {}
  abstract render(): string;
  abstract validate(): boolean;
}

class PersonalInfoStep extends WizardStep {
  private data = { name: "", email: "" };
  render() { return `<input name="name" /><input name="email" />`; }
  validate() {
    if (!this.data.name || !this.data.email) return false;
    this.mediator.notify(this.name, "valid", this.data);
    return true;
  }
}

class AddressStep extends WizardStep {
  private data = { street: "", city: "" };
  render() { return `<input name="street" /><input name="city" />`; }
  validate() {
    if (!this.data.street || !this.data.city) return false;
    this.mediator.notify(this.name, "valid", this.data);
    return true;
  }
}

class PaymentStep extends WizardStep {
  private data = { card: "", cvv: "" };
  render() { return `<input name="card" /><input name="cvv" />`; }
  validate() {
    if (this.data.card.length < 16) return false;
    this.mediator.notify(this.name, "valid", this.data);
    return true;
  }
}

// Concrete mediator
class CheckoutWizard implements WizardMediator {
  private steps: WizardStep[] = [];
  private currentStep = 0;
  private collectedData: Record<string, unknown> = {};

  constructor() {
    this.steps = [
      new PersonalInfoStep(this, "personal"),
      new AddressStep(this, "address"),
      new PaymentStep(this, "payment"),
    ];
  }

  notify(sender: string, event: string, data?: unknown) {
    if (event === "valid") {
      this.collectedData[sender] = data;
      this.next();
    }
  }

  next() {
    if (this.currentStep < this.steps.length - 1) {
      this.currentStep++;
      this.renderCurrent();
    } else {
      this.complete();
    }
  }

  back() {
    if (this.currentStep > 0) { this.currentStep--; this.renderCurrent(); }
  }

  renderCurrent(): string { return this.steps[this.currentStep].render(); }
  validateCurrent(): boolean { return this.steps[this.currentStep].validate(); }
  complete() { console.log("Wizard complete:", this.collectedData); }
}

// Usage: steps do not know each other
const wizard = new CheckoutWizard();
wizard.renderCurrent(); // Step 1: PersonalInfo
wizard.validateCurrent(); // -> notify -> next()
wizard.renderCurrent(); // Step 2: Address

Lessons:

  • Mediator coordinates wizard steps without coupling
  • Each step only knows the mediator, not other steps
  • Adding new step = new class + register in mediator
  • The mediator controls flow: next, back, complete
  • Data is centralized in the mediator

### Mediator vs Observer in forms?

Use Mediator in multi-step forms: the mediator controls flow (next, back, validate). Use Observer in reactive forms: the field notifies changes to observers (live validation, field dependencies). Mediator is centralized: the mediator decides. Observer is decentralized: each component reacts. For wizards, Mediator. For reactive forms, Observer.

Frequently Asked Questions

Is Mediator the same as Observer?

Related but different. Observer is a one-to-many dependency where subjects notify observers. Mediator centralizes many-to-many communication. An event bus can serve as both.

What is the difference between Mediator and Facade?

Facade provides a simplified interface to a subsystem. Mediator coordinates communication between peer objects. Facade is about simplifying access; Mediator is about decoupling peers.