Mediator: Untangling Communication Between Objects
Starting from a situation where objects call each other directly and the relationships turn into a tangled mess, see how the Mediator Pattern centralizes communication logic through a single mediator, and how it differs from the Observer Pattern.
When you're building screens with complicated interaction logic, it's easy to end up writing code like this: checking a checkbox needs to enable some input field, and typing into that input field needs to toggle whether a nearby button is clickable. Add enough objects like this, each holding direct references to the others and calling each other's methods, and the relationships between them turn into a tangled web fast. Change one thing, and you'll often accidentally break something that looks completely unrelated.
That's exactly the problem the Mediator Pattern solves: instead of letting objects know about each other and call each other directly, everything goes through a single mediator.
Just How Messy Direct Connections Get
Picture an airport where every plane has to coordinate its own takeoff and landing order directly with every other plane. As the number of planes grows, the coordination complexity explodes. That's exactly why airports have a control tower — every plane only talks to the tower, the tower handles all the coordination, and planes never need to talk to each other directly.
The same thing happens in code. Say you have a form where a checkbox, an input, and a button all need to react to each other. If you let them hold direct references to one another:
class Checkbox {
#input
#button
constructor(input, button) {
this.#input = input
this.#button = button
}
toggle(checked) {
this.#input.setEnabled(checked)
if (!checked) {
this.#button.setEnabled(false)
}
}
}
class Input {
#button
constructor(button) {
this.#button = button
}
setEnabled(enabled) {
console.log(`Input ${enabled ? 'enabled' : 'disabled'}`)
}
onInput(value) {
this.#button.setEnabled(value.length > 0)
}
}
class Button {
setEnabled(enabled) {
console.log(`Button ${enabled ? 'enabled' : 'disabled'}`)
}
}Checkbox needs to know about Input and Button, Input needs to know about Button, and as more components join the mix, this web of "who knows who" spreads out in every direction. Add a dropdown later, and if it needs to react with any existing component, you have to go back and modify that component so it knows about the new one too. One small addition, and the blast radius touches everything.
Bring In a Mediator So Components Only Know About It
The Mediator Pattern's approach is to stop components from knowing about each other directly. Each one only talks to the mediator, and all the actual coordination logic lives inside that mediator:
class FormMediator {
#checkbox
#input
#button
register({ checkbox, input, button }) {
this.#checkbox = checkbox
this.#input = input
this.#button = button
}
notify(sender, event, payload) {
if (sender === 'checkbox' && event === 'toggle') {
this.#input.setEnabled(payload.checked)
if (!payload.checked) {
this.#button.setEnabled(false)
}
}
if (sender === 'input' && event === 'change') {
this.#button.setEnabled(payload.value.length > 0)
}
}
}
class Checkbox {
#mediator
constructor(mediator) {
this.#mediator = mediator
}
toggle(checked) {
this.#mediator.notify('checkbox', 'toggle', { checked })
}
}
class Input {
#mediator
constructor(mediator) {
this.#mediator = mediator
}
setEnabled(enabled) {
console.log(`Input ${enabled ? 'enabled' : 'disabled'}`)
}
onInput(value) {
this.#mediator.notify('input', 'change', { value })
}
}
class Button {
setEnabled(enabled) {
console.log(`Button ${enabled ? 'enabled' : 'disabled'}`)
}
}Here's how it's wired up in practice:
const mediator = new FormMediator()
const checkbox = new Checkbox(mediator)
const input = new Input(mediator)
const button = new Button()
mediator.register({ checkbox, input, button })
checkbox.toggle(true)
// Input enabled
input.onInput('hello')
// Button enabled
checkbox.toggle(false)
// Input disabled
// Button disabledCheckbox, Input, and Button no longer hold any references to each other — the only thing any of them knows about is FormMediator. Adding a dropdown later, or changing how these components react to each other, only means touching the logic inside FormMediator.notify(). The components themselves don't need to change at all.
Mediator vs. Observer: They Look Alike, But Solve Different Problems
If you've read our earlier piece on the Observer Pattern, that notify() method up there probably looks familiar. The two patterns are easy to mix up, but they're focused on different things:
- Observer Pattern: it's about one-to-many notification. A Subject broadcasts a state change, and each Observer independently decides how to react. The Subject itself never gets involved in coordinating what the Observers do.
- Mediator Pattern: it's about many-to-many coordination. The mediator doesn't just forward messages — it actively decides "when A does X, B and C should respond with Y and Z." That coordination logic lives inside the mediator.
Back to the form example: FormMediator doesn't just relay the checkbox's event outward. It has "when the checkbox gets unchecked, the button should also get disabled" baked right into it as business logic. That's the real difference between Mediator and plain event broadcasting — the mediator owns the decision-making, not just the message-passing.
The Trade-off: A Mediator Can Turn Into a New God Object
The Mediator Pattern takes coupling that used to be scattered across many objects and concentrates it in one place — and that comes with a side effect. If the coordination logic keeps growing, the mediator can easily balloon into a God Object that knows about and controls everything, the same risk we called out with the Facade Pattern.
The difference is really just where the complexity ends up living. Spreading it across components that all reference each other, or concentrating it in one mediator — both approaches still have to carry the same amount of complexity somewhere. The real value of the Mediator Pattern isn't making complexity disappear; it's making it traceable. Want to know how a form's fields react to each other? Just read FormMediator. You don't have to go hunting through the whole codebase for who references who.
When It's a Good Fit
- Multiple components have complicated many-to-many relationships that keep changing as requirements evolve
- You want to avoid components holding direct references to each other, so each one can be tested and swapped independently
- The coordination logic carries real business meaning and deserves to be read and maintained as a single, centralized piece
If the relationship between components is really just "A does something, B passively receives a notification" with no real decision-making involved, the Observer Pattern's broadcast mechanism is usually enough on its own — you don't need to add a Mediator layer on top.
Wrapping Up
The core value of the Mediator Pattern is turning a tangled, mesh-like web of communication between objects into a clean star shape: every object only knows about the mediator, and the actual coordination decisions live inside it. The difference from the Observer Pattern comes down to this: a Mediator actively owns coordination decisions, while an Observer's Subject just forwards notifications.