Mediator Pattern
Define an object that encapsulates how a set of objects interact. A behavioral design pattern for reducing chaotic dependencies.
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
| Variant | Description | Use Case |
|---|---|---|
| Event Bus | Decoupled pub/sub via a central channel | Large systems with many publishers/subscribers |
| Command Bus | Commands routed through a central handler | CQRS, task dispatching |
| Dialog Director | UI widgets coordinated by a dialog controller | Form 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.
Related Resources
Observer Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
PatternState Pattern
Allow an object to alter its behavior when its internal state changes. A behavioral design pattern for finite state machines.
PatternSingleton Pattern
Ensure a class has only one instance and provide global access to it. A creational design pattern for controlled object creation.
PatternEvent Bus Pattern
Decouple components by routing events through a central bus. A behavioral pattern for loosely coupled communication between modules.
PatternFacade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
GuideVertical Slice Architecture: Feature-First Organization
A practical guide to Vertical Slice Architecture: organize code by feature instead of technical concern, reducing cross-layer navigation and improving cohesion.