Skip to content
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

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Mediator Pattern

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

Frequently Asked Questions

Q: Is Mediator the same as Observer? A: 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.

Q: What is the difference between Mediator and Facade? A: Facade provides a simplified interface to a subsystem. Mediator coordinates communication between peer objects. Facade is about simplifying access; Mediator is about decoupling peers.

Is this pattern suitable for small projects?

For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.

How does this pattern compare to alternatives?

Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.

Can I partially apply this pattern?

Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.

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.