Mediator
Mediator is a behavioral pattern that pulls the web of interactions between a group of objects out into a separate go-between object, so the objects themselves no longer need to know about each other.
Problem
A group chat app lets several users exchange messages. If every user object calls every other user object directly to deliver a message, each user ends up hardcoding references to everyone else currently in the room.
It works with three people, but drop in a fourth and you have to rewire every existing user to know about them. Remove someone and the same rewiring happens in reverse. The web of direct references becomes a tangle that's hard to follow and even harder to change.
Solution
Mediator pulls all the cross-references out of the users and moves them into one dedicated coordinator object - the chat room. Users stop calling each other directly; instead, each one sends its message to the chat room through a single method, and the chat room looks up everyone currently registered and delivers the message to them.
A user that wants to say something now just tells the chat room and forgets about who's listening. The chat room is the only place that knows the full list of participants, so users can join or leave without any other user's code changing.
When to Use
- Reach for Mediator when a set of objects references so many of each other's methods that changing one ripples through the rest.
- Use it when a component can't be dropped into a new context because it's wired directly into a specific set of collaborators.
- Consider it when subclassing components just to vary how they react to each other has produced a pile of near-duplicate classes.
Real-World Examples
- **Air traffic control towers** - aircraft never negotiate runway access with each other; every request and clearance passes through the tower.
- **React Context / centralized state stores** - components dispatch actions to a shared store instead of passing callbacks down through every sibling.
- **Message brokers like RabbitMQ** - publishers and consumers never connect to each other directly; the broker routes every message between them.
Structure
Components no longer hold references to each other - only to the mediator. Each component reports events through a narrow mediator interface, and the concrete mediator, which knows about every component it coordinates, contains the actual reaction logic.
Participants
A narrow interface, typically one method, through which components report what just happened to them without knowing who will act on it.
Keeps references to the components it coordinates and contains the actual reaction rules - which components to update and how, in response to a reported event.
In the diagram: Chat Room.
Only knows about the mediator, never about sibling components. Reports its own state changes and reacts only to calls the mediator makes on it.
In the diagram: Alice, Bob, Charlie, Diana.
class ChatMediator {
register(user) {}
sendMessage(message, sender) {}
}
class ChatRoom extends ChatMediator {
#users = [];
register(user) {
this.#users.push(user);
}
sendMessage(message, sender) {
for (const user of this.#users) {
if (user !== sender) user.receive(message, sender);
}
}
}
class User {
constructor(name, mediator) {
this.name = name;
this.mediator = mediator;
mediator.register(this);
}
send(message) {
console.log(`${this.name} sends: ${message}`);
this.mediator.sendMessage(message, this);
}
receive(message, sender) {
console.log(`${this.name} receives from ${sender.name}: ${message}`);
}
}
const room = new ChatRoom();
const alice = new User('Alice', room);
const bob = new User('Bob', room);
const charlie = new User('Charlie', room);
const diana = new User('Diana', room);
alice.send('Hello everyone!');
Step 1 of 5
ChatMediator declares the interface colleagues talk through
`register(user)` and `sendMessage(message, sender)` are empty stubs here - concrete mediators implement the actual routing, but users only ever depend on this shared shape.
Step 2 of 5
ChatRoom is the concrete mediator holding all the coupling
`#users` is the only place that knows about every participant; `sendMessage` loops over `#users` and calls `user.receive(...)` on everyone except `sender` - users never call each other directly.
Step 3 of 5
User only knows its mediator, not the other users
The constructor calls `mediator.register(this)` to join the room; `send` calls `this.mediator.sendMessage(message, this)` instead of contacting anyone directly; `receive` just logs what arrives - all cross-user communication is routed through `mediator`.
Step 4 of 5
Four users register with the same mediator instance
Each `new User(name, room)` call passes the same `room`, and each constructor immediately registers itself - `room.#users` ends up with all four, without any user referencing another.
Step 5 of 5
One send() fans out to everyone else through the mediator
`alice.send(...)` triggers `room.sendMessage(...)`, which calls `receive` on Bob, Charlie and Diana but skips Alice herself - Alice never had to know who else was in the room.
interface ChatMediator {
register(user: User): void;
sendMessage(message: string, sender: User): void;
}
class ChatRoom implements ChatMediator {
private users: User[] = [];
register(user: User): void {
this.users.push(user);
}
sendMessage(message: string, sender: User): void {
for (const user of this.users) {
if (user !== sender) user.receive(message, sender);
}
}
}
class User {
constructor(public name: string, private mediator: ChatMediator) {
mediator.register(this);
}
send(message: string): void {
console.log(`${this.name} sends: ${message}`);
this.mediator.sendMessage(message, this);
}
receive(message: string, sender: User): void {
console.log(`${this.name} receives from ${sender.name}: ${message}`);
}
}
const room = new ChatRoom();
const alice = new User('Alice', room);
const bob = new User('Bob', room);
const charlie = new User('Charlie', room);
const diana = new User('Diana', room);
alice.send('Hello everyone!');
interface ChatMediator {
void register(User user);
void sendMessage(String message, User sender);
}
class ChatRoom implements ChatMediator {
private final List<User> users = new ArrayList<>();
public void register(User user) {
users.add(user);
}
public void sendMessage(String message, User sender) {
for (User user : users) {
if (user != sender) user.receive(message, sender);
}
}
}
class User {
String name;
ChatMediator mediator;
User(String name, ChatMediator mediator) {
this.name = name;
this.mediator = mediator;
mediator.register(this);
}
void send(String message) {
System.out.println(name + " sends: " + message);
mediator.sendMessage(message, this);
}
void receive(String message, User sender) {
System.out.println(name + " receives from " + sender.name + ": " + message);
}
}
ChatRoom room = new ChatRoom();
User alice = new User("Alice", room);
User bob = new User("Bob", room);
User charlie = new User("Charlie", room);
User diana = new User("Diana", room);
alice.send("Hello everyone!");
interface IChatMediator
{
void Register(User user);
void SendMessage(string message, User sender);
}
class ChatRoom : IChatMediator
{
private readonly List<User> users = new();
public void Register(User user) => users.Add(user);
public void SendMessage(string message, User sender)
{
foreach (var user in users)
{
if (user != sender) user.Receive(message, sender);
}
}
}
class User
{
public string Name;
private readonly IChatMediator mediator;
public User(string name, IChatMediator mediator)
{
Name = name;
this.mediator = mediator;
mediator.Register(this);
}
public void Send(string message)
{
Console.WriteLine($"{Name} sends: {message}");
mediator.SendMessage(message, this);
}
public void Receive(string message, User sender)
{
Console.WriteLine($"{Name} receives from {sender.Name}: {message}");
}
}
var room = new ChatRoom();
var alice = new User("Alice", room);
var bob = new User("Bob", room);
var charlie = new User("Charlie", room);
var diana = new User("Diana", room);
alice.Send("Hello everyone!");
from __future__ import annotations
from abc import ABC, abstractmethod
class ChatMediator(ABC):
@abstractmethod
def register(self, user: 'User') -> None: ...
@abstractmethod
def send_message(self, message: str, sender: 'User') -> None: ...
class ChatRoom(ChatMediator):
def __init__(self) -> None:
self._users: list['User'] = []
def register(self, user: 'User') -> None:
self._users.append(user)
def send_message(self, message: str, sender: 'User') -> None:
for user in self._users:
if user is not sender:
user.receive(message, sender)
class User:
def __init__(self, name: str, mediator: ChatMediator) -> None:
self.name = name
self.mediator = mediator
mediator.register(self)
def send(self, message: str) -> None:
print(f'{self.name} sends: {message}')
self.mediator.send_message(message, self)
def receive(self, message: str, sender: 'User') -> None:
print(f'{self.name} receives from {sender.name}: {message}')
room = ChatRoom()
alice = User('Alice', room)
bob = User('Bob', room)
charlie = User('Charlie', room)
diana = User('Diana', room)
alice.send('Hello everyone!')
Step 1 of 5
ChatMediator declares the interface colleagues talk through
`register` and `send_message` are both `@abstractmethod` - concrete mediators implement the actual routing, but `User` only ever depends on this shared interface.
Step 2 of 5
ChatRoom is the concrete mediator holding all the coupling
`_users` is the only place that knows about every participant; `send_message` loops over `_users` and calls `user.receive(...)` on everyone except `sender` - users never call each other directly.
Step 3 of 5
User only knows its mediator, not the other users
`__init__` calls `mediator.register(self)` to join the room; `send` calls `self.mediator.send_message(message, self)` instead of contacting anyone directly; `receive` just prints what arrives.
Step 4 of 5
Four users register with the same mediator instance
Each `User(name, room)` call passes the same `room`, and each constructor immediately registers itself - `room._users` ends up with all four, without any user referencing another.
Step 5 of 5
One send() fans out to everyone else through the mediator
`alice.send(...)` triggers `room.send_message(...)`, which calls `receive` on Bob, Charlie and Diana but skips Alice herself - Alice never had to know who else was in the room.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- Cross-component logic lives in one class instead of being smeared across every participant, so you can read the whole reaction chain in one place.
- Components can be swapped, tested, or reused independently, since none of them hold references to the others.
- Adding a new coordination rule usually means editing the mediator, not hunting through every component that might be affected.
- New component types can be plugged in by teaching the mediator about them, without touching the components that already work.
Disadvantages
- All the complexity that used to be spread across components now concentrates in the mediator, which can balloon into a class that knows and does too much.
- Debugging means tracing calls through an extra indirection layer instead of following a direct method call between two objects.
- For a small, stable set of interactions the mediator can feel like unnecessary ceremony compared to a couple of direct calls.
Question 1 of 10
In the structure, what does a Component (Colleague) know about, and what does it not know about?
Correct! A Component only knows about the mediator, never about sibling components - it reports its own state changes and reacts only to calls the mediator makes on it.
Not quite. A Component only knows about the mediator, never about sibling components - it reports its own state changes and reacts only to calls the mediator makes on it.
Question 2 of 10
In the group-chat example, what happens as soon as a fourth user joins a chat built on direct references?
Correct! It works with three people, but drop in a fourth and you have to rewire every existing user to know about them - the web of direct references becomes a tangle that's hard to change.
Not quite. It works with three people, but drop in a fourth and you have to rewire every existing user to know about them - the web of direct references becomes a tangle that's hard to change.
Question 3 of 10
Which of these is listed as a genuine benefit of Mediator?
Correct! Cross-component logic lives in one class instead of being smeared across every participant, so you can read the whole reaction chain in one place.
Not quite. Cross-component logic lives in one class instead of being smeared across every participant, so you can read the whole reaction chain in one place.
Question 4 of 10
Why is debugging harder in a system built around Mediator?
Correct! Debugging means tracing calls through an extra indirection layer instead of following a direct method call between two objects.
Not quite. Debugging means tracing calls through an extra indirection layer instead of following a direct method call between two objects.
Question 5 of 10
How does a User join the chat room in the implementation?
Correct! The User constructor calls mediator.register(this) to join the room - each new User passes the same mediator instance and registers itself immediately.
Not quite. The User constructor calls mediator.register(this) to join the room - each new User passes the same mediator instance and registers itself immediately.
Question 6 of 10
Once Mediator is applied, how does a user actually send a message to the group?
Correct! A user that wants to say something now just tells the chat room and forgets about who's listening - the chat room is the only place that knows the full list of participants.
Not quite. A user that wants to say something now just tells the chat room and forgets about who's listening - the chat room is the only place that knows the full list of participants.
Question 7 of 10
What is a stated risk of concentrating coordination logic in the mediator?
Correct! All the complexity that used to be spread across components now concentrates in the mediator, which can balloon into a class that knows and does too much.
Not quite. All the complexity that used to be spread across components now concentrates in the mediator, which can balloon into a class that knows and does too much.
Question 8 of 10
When can introducing a Mediator feel like unnecessary ceremony?
Correct! For a small, stable set of interactions the mediator can feel like unnecessary ceremony compared to a couple of direct calls.
Not quite. For a small, stable set of interactions the mediator can feel like unnecessary ceremony compared to a couple of direct calls.
Question 9 of 10
What does Mediator pull out of a group of interacting objects?
Correct! Mediator pulls the web of interactions between a group of objects out into a separate go-between object, so the objects themselves no longer need to know about each other.
Not quite. Mediator pulls the web of interactions between a group of objects out into a separate go-between object, so the objects themselves no longer need to know about each other.
Question 10 of 10
In the JavaScript ChatRoom implementation, what does sendMessage do with the sender argument?
Correct! sendMessage loops over #users and calls user.receive(...) on everyone except sender - users never call each other directly, and the sender doesn't receive its own message back.
Not quite. sendMessage loops over #users and calls user.receive(...) on everyone except sender - users never call each other directly, and the sender doesn't receive its own message back.