Mediator Pattern
Defines an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly and lets you vary their interaction independently. Components only know about the mediator, not each other.
✦ Problem
When many components communicate directly with each other, the resulting web of dependencies makes the system hard to understand and modify. Each component must know about all the others it interacts with. Adding a new component requires modifying all related components. This tangled coupling turns components into monoliths that can't be reused independently.
✦ Solution
Force all communication between components to go through a single mediator object. Components become unaware of each other. They only interact with the mediator. When something happens in a component, it notifies the mediator, which decides which other components to notify. This reduces dependencies from many-to-many to one-to-many.
Participants
| Role | Responsibility |
|---|---|
| Mediator | Defines an interface for communicating with Colleague objects |
| ConcreteMediator | Coordinates communication between Colleague objects |
| Colleague | Each colleague communicates with its mediator instead of other colleagues directly |
TypeScript Example
interface ChatMediator {
sendMessage(message: string, sender: User): void;
addUser(user: User): void;
}
class User {
constructor(public name: string, private mediator: ChatMediator) {
mediator.addUser(this);
}
send(message: string) {
console.log(`${this.name} sends: ${message}`);
this.mediator.sendMessage(message, this);
}
receive(message: string, from: string) {
console.log(`${this.name} received from ${from}: ${message}`);
}
}
class ChatRoom implements ChatMediator {
private users: User[] = [];
addUser(user: User) {
this.users.push(user);
}
sendMessage(message: string, sender: User) {
for (const user of this.users) {
if (user !== sender) {
user.receive(message, sender.name);
}
}
}
}
const room = new ChatRoom();
const alice = new User("Alice", room);
const bob = new User("Bob", room);
const charlie = new User("Charlie", room);
alice.send("Hey everyone!");
bob.send("Hi Alice!");Real-World Example
Air traffic control towers are the canonical Mediator example—planes don't communicate directly with each other; instead, the tower coordinates takeoffs, landings, and taxiing. In software, chat rooms mediate between users. Event buses (Redux, Vuex) mediate between UI components. Express.js middleware chains mediate between request handlers. Form validators mediate interactions between form fields.
✓ When to Use
- •A set of objects communicate in well-defined but complex ways that are hard to understand
- •Reusing an object is difficult because it refers to many other objects
- •You want to customize behavior distributed between several classes without creating many subclasses
- •You need to reduce chaotic dependencies between tightly coupled components
✗ When Not to Use
- •Components have simple, direct interactions that don't benefit from indirection
- •The mediator becomes a god object with too much logic and too many responsibilities
- •Performance is critical and the extra routing through a mediator adds unacceptable latency
Understanding the Mediator Pattern
The Mediator pattern is one of the most important behavioral design patterns in object-oriented software design. Originally cataloged by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their seminal 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software," this pattern continues to be a cornerstone of modern software architecture.
Defines an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly and lets you vary their interaction independently. Components only know about the mediator, not each other. The pattern involves the following key participants: Mediator, ConcreteMediator, Colleague. Each participant has a well-defined role, and the interactions between them create a flexible, maintainable structure that can evolve with changing requirements.
In TypeScript and modern JavaScript development, the Mediator pattern is particularly valuable because the language's type system and interface support allow you to express the pattern with compile-time safety. TypeScript generics can make Mediator implementations even more flexible and reusable while maintaining strong type checking throughout the pattern's structure.
Knowing when to apply the Mediator pattern is as important as knowing how. Common use cases include: A set of objects communicate in well-defined but complex ways that are hard to understand; Reusing an object is difficult because it refers to many other objects; You want to customize behavior distributed between several classes without creating many subclasses. However, overusing design patterns can lead to unnecessarily complex code. Always evaluate whether the pattern genuinely solves your problem or if a simpler approach would suffice.
The Mediator pattern is related to several other design patterns: Observer, Facade, Command. Understanding these relationships helps you choose the right pattern for your specific situation and combine patterns effectively when building complex systems.
Related Patterns
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified …
Provides a simplified interface to a library, framework, or complex subsystem. A facade doesn't encapsulate the subsyste…
Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requ…
More Behavioral Patterns
Explore All Design Patterns
Browse our complete reference of 23 design patterns with TypeScript examples, UML participants, and practical guidance.