Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Mediator
Stop objects from talking to each other directly. Route their communication through one coordinator, so each object only knows the mediator.
The problem
A sign-up form has a "Company account" checkbox, a company-name field, a VAT field, a country dropdown and a submit button. When the checkbox changes, the company fields show or hide. When the country changes, the VAT field's rules change. The submit button enables only when everything is valid.
Each component holds references to the others and updates them directly. There are now 20 links between 5 components; reusing the checkbox on another form drags all its neighbours along, and nobody can say where the "enable submit" rule lives.
Real-world analogy
An airport control tower. Pilots don't talk to every other plane to decide who lands first. Each pilot talks only to the tower, and the tower coordinates everyone. Planes stay simple; the coordination rules live in one place.
Without the pattern
class CompanyCheckbox:
def toggle(self, checked):
self.company_field.visible = checked # knows about the field...
self.vat_field.visible = checked # ...and the VAT field...
self.submit.enabled = self.form_valid() # ...and the submit button
The pattern
- Mediator: knows all the components and contains the coordination rules ("when the checkbox changes, show the company fields and re-check the submit button").
- Components: know only the mediator. When something happens, they
notify(self, event)and let the mediator decide what else should happen.
Many-to-many links between components become one-to-many links through the mediator.
The code
A chat room is the classic, easy-to-follow mediator: users send messages to the room, and the room decides who receives them, including private messages and muted users.
class ChatRoom: # mediator: the only place that knows who talks to whom
def __init__(self):
self.users, self.muted = {}, set()
def join(self, user):
self.users[user.name] = user
user.room = self
def send(self, sender, text, to=None):
if sender.name in self.muted:
sender.receive("room", "you are muted")
return
targets = [self.users[to]] if to else [u for u in self.users.values() if u is not sender]
for user in targets:
user.receive(sender.name, text)
class User: # component: knows only the room
def __init__(self, name):
self.name, self.room = name, None
def say(self, text, to=None):
self.room.send(self, text, to)
def receive(self, sender, text):
print(f"{self.name} <- {sender}: {text}")
room = ChatRoom()
ada, bob, cy = User("ada"), User("bob"), User("cy")
for user in [ada, bob, cy]:
room.join(user)
ada.say("standup in 5 minutes")
bob.say("can you review my PR?", to="cy")
room.muted.add("cy")
cy.say("spam spam spam")
Output
bob <- ada: standup in 5 minutes cy <- ada: standup in 5 minutes cy <- bob: can you review my PR? cy <- room: you are muted
class ChatRoom { // mediator: the only place that knows who talks to whom
private users = new Map<string, User>();
muted = new Set<string>();
join(user: User) {
this.users.set(user.name, user);
user.room = this;
}
send(sender: User, text: string, to?: string) {
if (this.muted.has(sender.name)) {
sender.receive("room", "you are muted");
return;
}
const targets = to ? [this.users.get(to)!] : [...this.users.values()].filter((u) => u !== sender);
for (const user of targets) user.receive(sender.name, text);
}
}
class User { // component: knows only the room
room!: ChatRoom;
constructor(readonly name: string) {}
say(text: string, to?: string) { this.room.send(this, text, to); }
receive(sender: string, text: string) { console.log(`${this.name} <- ${sender}: ${text}`); }
}
const room = new ChatRoom();
const [ada, bob, cy] = [new User("ada"), new User("bob"), new User("cy")];
for (const user of [ada, bob, cy]) room.join(user);
ada.say("standup in 5 minutes");
bob.say("can you review my PR?", "cy");
room.muted.add("cy");
cy.say("spam spam spam");
Output
bob <- ada: standup in 5 minutes cy <- ada: standup in 5 minutes cy <- bob: can you review my PR? cy <- room: you are muted
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) {
ChatRoom room = new ChatRoom();
User ada = new User("ada"), bob = new User("bob"), cy = new User("cy");
for (User user : List.of(ada, bob, cy)) room.join(user);
ada.say("standup in 5 minutes", null);
bob.say("can you review my PR?", "cy");
room.muted.add("cy");
cy.say("spam spam spam", null);
}
}
class ChatRoom { // mediator: the only place that knows who talks to whom
private final Map<String, User> users = new LinkedHashMap<>();
final Set<String> muted = new HashSet<>();
void join(User user) {
users.put(user.name, user);
user.room = this;
}
void send(User sender, String text, String to) {
if (muted.contains(sender.name)) {
sender.receive("room", "you are muted");
return;
}
List<User> targets = to != null ? List.of(users.get(to))
: users.values().stream().filter(u -> u != sender).toList();
for (User user : targets) user.receive(sender.name, text);
}
}
class User { // component: knows only the room
final String name;
ChatRoom room;
User(String name) { this.name = name; }
void say(String text, String to) { room.send(this, text, to); }
void receive(String sender, String text) { System.out.println(name + " <- " + sender + ": " + text); }
}
Output
bob <- ada: standup in 5 minutes cy <- ada: standup in 5 minutes cy <- bob: can you review my PR? cy <- room: you are muted
Run this on your computer — see “Get Started” for setup.
<?php
class ChatRoom { // mediator: the only place that knows who talks to whom
private array $users = [];
public array $muted = [];
public function join(User $user): void {
$this->users[$user->name] = $user;
$user->room = $this;
}
public function send(User $sender, string $text, ?string $to = null): void {
if (in_array($sender->name, $this->muted, true)) {
$sender->receive("room", "you are muted");
return;
}
$targets = $to ? [$this->users[$to]] : array_filter($this->users, fn(User $u) => $u !== $sender);
foreach ($targets as $user) $user->receive($sender->name, $text);
}
}
class User { // component: knows only the room
public ChatRoom $room;
public function __construct(public readonly string $name) {}
public function say(string $text, ?string $to = null): void { $this->room->send($this, $text, $to); }
public function receive(string $sender, string $text): void { echo "{$this->name} <- $sender: $text\n"; }
}
$room = new ChatRoom();
[$ada, $bob, $cy] = [new User("ada"), new User("bob"), new User("cy")];
foreach ([$ada, $bob, $cy] as $user) $room->join($user);
$ada->say("standup in 5 minutes");
$bob->say("can you review my PR?", "cy");
$room->muted[] = "cy";
$cy->say("spam spam spam");
Output
bob <- ada: standup in 5 minutes cy <- ada: standup in 5 minutes cy <- bob: can you review my PR? cy <- room: you are muted
Users never reference each other. Muting, private messages and any future rule (rate limits, moderation) live in the room.
When to use it
- Many objects communicate in complex, many-to-many ways, and the links make them hard to change or reuse: form controls, dialog widgets, chat participants, game units.
- You want the coordination rules in one place instead of spread across components.
When not to use it
- Only two or three objects interact simply. Direct calls are clearer.
- The mediator can grow into a god object that knows everything. Split it by area (one mediator per dialog or feature).
- If components just need to announce events without a central coordinator, Observer or an event bus may be lighter.
You've already used it
- Form libraries and dialog controllers: a form object coordinates field validation and the submit button's state.
- Chat servers and message hubs: SignalR hubs and Socket.IO rooms route messages between clients.
- Air-traffic-style orchestration: workflow engines and orchestrators (Step Functions, Temporal) coordinate services that don't call each other.
- MediatR in .NET dispatches requests to handlers through one mediator object.
Key points
- Mediator moves communication from object-to-object to object-to-mediator.
- Components become simpler and reusable; coordination rules live in one place.
- Watch for the mediator becoming a god object; keep one per feature.
- Further reading: Refactoring Guru: Mediator.
Practice questions
What problem does the Mediator pattern solve?
Answer
Tangled many-to-many communication between objects. Instead of components referencing and updating each other, each one talks only to a mediator, which holds the coordination rules. Components become simpler and easier to reuse.
How is Mediator different from Observer?
Answer
With Observer, a subject broadcasts events to any subscribers without knowing what they do; there is no central brain. With Mediator, a central object knows the participants and decides how they interact. An event bus sits in between and is often used to implement a mediator.
What is the main risk of the Mediator pattern?
Answer
The mediator can grow into a god object that knows about everything and becomes hard to change. Keep mediators focused, for example one per dialog, form or feature.
Open in the app to mark lessons complete, take notes and track your progress.