Intent
Define an object that encapsulates how a set of related objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly — and lets you vary their interactions independently.
The Problem
You're building a registration form. The form has interconnected fields:
- A country dropdown.
- A region dropdown that shows different options depending on the country.
- A VAT-number field that's required only for EU countries with a business account type.
- An account type radio (personal/business).
- A submit button that only enables when everything's valid.
The first version wires everything together directly:
country.onChange(v => {
region.setOptions(regionsFor(v));
vatField.setVisible(EU.includes(v) && accountType.value === "business");
submit.setEnabled(form.isValid());
});
accountType.onChange(v => {
vatField.setVisible(EU.includes(country.value) && v === "business");
submit.setEnabled(form.isValid());
});
// ... and so on, for region, vatField, every other widget.
Each widget knows about every other widget. The country dropdown imports the region dropdown imports the VAT field imports the submit button. Adding a language field that depends on country means editing every existing handler. Pulling the form apart for tests is impossible — every widget drags every other widget along.
The shape of the smell: N objects, each knowing about all the others, with the coordination rules scattered across N event handlers.
The Solution
Mediator says: pick one object whose job is to coordinate. Every widget reports to it; the mediator knows the rules and pushes updates back out. Widgets only know about the mediator interface — never about each other.
interface FormMediator {
notify(sender: string, event: string): void;
}
class RegistrationForm implements FormMediator {
// ... holds references to every widget ...
notify(sender: string, event: string): void {
if (sender === "country" && event === "change") {
this.region.setOptions(regionsFor(this.country.value));
this.updateVatVisibility();
}
if (sender === "accountType" && event === "change") {
this.updateVatVisibility();
}
this.submit.setEnabled(this.isValid());
}
private updateVatVisibility(): void {
const visible = EU.includes(this.country.value)
&& this.accountType.value === "business";
this.vatField.setVisible(visible);
}
}
Now every widget calls mediator.notify("me", "change"). The widgets stop importing each other. The rules of the form live in one class — testable, swappable, knowable.
Real-World Analogy
An air traffic control tower. In a busy airport's airspace, dozens of aircraft are trying to take off, land, and transit. If every pilot had to coordinate directly with every other pilot — "I need to land in two minutes, can you wait?" — the system would seize up before the third aircraft entered the picture.
Instead, every aircraft talks only to the tower. The tower has the bigger picture: weather, runway availability, other aircraft positions, fuel constraints. It tells aircraft what to do — hold at flight level 250, descend to 15,000 feet, you're cleared to land on runway 24 left. The aircraft don't know each other's call signs; they only know the tower's frequency.
That's Mediator in a sentence: many things, one coordinator, no direct chatter.
Structure
Three roles you'll see in every Mediator implementation:
- Mediator — the interface every coordinator implements. Often just
notify(sender, event)or per-event methods. Here:FormMediator. - Concrete Mediator — the specific coordinator that knows the rules. Holds references to every colleague it coordinates. Here:
RegistrationForm. - Colleagues — the related objects. Each holds a reference to the mediator. They report events to the mediator and let the mediator decide what to do. Here:
CountryDropdown,RegionDropdown,VatField,AccountTypeRadio,SubmitButton.
The defining property: colleagues never reference each other. They only reference the mediator. Coordination rules live in one place.
Code Examples
Here's the registration-form mediator in five languages. Watch how each colleague becomes simple — it knows its own value and how to render itself, and otherwise just notifies the mediator.
interface FormMediator {
notify(sender: string, event: string): void;
}
class CountryDropdown {
value = "";
constructor(private mediator: FormMediator) {}
setValue(v: string): void {
this.value = v;
this.mediator.notify("country", "change");
}
}
class VatField {
visible = false;
setVisible(v: boolean): void { this.visible = v; }
}
class SubmitButton {
enabled = false;
setEnabled(e: boolean): void { this.enabled = e; }
}
class RegistrationForm implements FormMediator {
country = new CountryDropdown(this);
accountType = { value: "personal" };
vatField = new VatField();
submit = new SubmitButton();
notify(sender: string, event: string): void {
if (sender === "country" || sender === "accountType") {
const visible = EU.includes(this.country.value)
&& this.accountType.value === "business";
this.vatField.setVisible(visible);
}
this.submit.setEnabled(this.isValid());
}
isValid(): boolean { /* ... */ return true; }
}
from abc import ABC, abstractmethod
class FormMediator(ABC):
@abstractmethod
def notify(self, sender, event): ...
class CountryDropdown:
def __init__(self, mediator):
self.mediator = mediator
self.value = ""
def set_value(self, v):
self.value = v
self.mediator.notify("country", "change")
class VatField:
def __init__(self):
self.visible = False
def set_visible(self, v): self.visible = v
class SubmitButton:
def __init__(self):
self.enabled = False
def set_enabled(self, e): self.enabled = e
class RegistrationForm(FormMediator):
EU = {"DE", "FR", "IT", "PL", "UA"}
def __init__(self):
self.country = CountryDropdown(self)
self.account_type = "personal"
self.vat = VatField()
self.submit = SubmitButton()
def notify(self, sender, event):
if sender in ("country", "accountType"):
visible = self.country.value in self.EU and self.account_type == "business"
self.vat.set_visible(visible)
self.submit.set_enabled(self._is_valid())
def _is_valid(self): return True
public interface FormMediator {
void notify(String sender, String event);
}
public final class CountryDropdown {
private final FormMediator mediator;
private String value = "";
public CountryDropdown(FormMediator mediator) { this.mediator = mediator; }
public String value() { return value; }
public void setValue(String v) {
this.value = v;
mediator.notify("country", "change");
}
}
public final class RegistrationForm implements FormMediator {
private static final Set<String> EU = Set.of("DE", "FR", "IT", "PL", "UA");
private final CountryDropdown country = new CountryDropdown(this);
private String accountType = "personal";
private final VatField vat = new VatField();
private final SubmitButton submit = new SubmitButton();
@Override
public void notify(String sender, String event) {
if (sender.equals("country") || sender.equals("accountType")) {
boolean visible = EU.contains(country.value())
&& accountType.equals("business");
vat.setVisible(visible);
}
submit.setEnabled(isValid());
}
private boolean isValid() { return true; }
}
<?php
namespace App\Forms;
interface FormMediator
{
public function notify(string $sender, string $event): void;
}
final class CountryDropdown
{
public string $value = '';
public function __construct(private FormMediator $mediator) {}
public function setValue(string $v): void
{
$this->value = $v;
$this->mediator->notify('country', 'change');
}
}
final class RegistrationForm implements FormMediator
{
private const EU = ['DE', 'FR', 'IT', 'PL', 'UA'];
public CountryDropdown $country;
public string $accountType = 'personal';
public VatField $vat;
public SubmitButton $submit;
public function __construct()
{
$this->country = new CountryDropdown($this);
$this->vat = new VatField();
$this->submit = new SubmitButton();
}
public function notify(string $sender, string $event): void
{
if (in_array($sender, ['country', 'accountType'], true)) {
$visible = in_array($this->country->value, self::EU, true)
&& $this->accountType === 'business';
$this->vat->setVisible($visible);
}
$this->submit->setEnabled($this->isValid());
}
private function isValid(): bool { return true; }
}
package forms
type FormMediator interface {
Notify(sender, event string)
}
type CountryDropdown struct {
mediator FormMediator
Value string
}
func (c *CountryDropdown) SetValue(v string) {
c.Value = v
c.mediator.Notify("country", "change")
}
type RegistrationForm struct {
Country *CountryDropdown
AccountType string
Vat VatField
Submit SubmitButton
}
var eu = map[string]bool{"DE": true, "FR": true, "IT": true, "PL": true, "UA": true}
func NewRegistrationForm() *RegistrationForm {
f := &RegistrationForm{AccountType: "personal"}
f.Country = &CountryDropdown{mediator: f}
return f
}
func (f *RegistrationForm) Notify(sender, event string) {
if sender == "country" || sender == "accountType" {
visible := eu[f.Country.Value] && f.AccountType == "business"
f.Vat.SetVisible(visible)
}
f.Submit.SetEnabled(f.isValid())
}
func (f *RegistrationForm) isValid() bool { return true }
The colleagues are tiny — they hold their own value, render themselves, and notify on change. The orchestration logic — what should happen when the country changes — lives in one place that you can read top to bottom.
When to Use It
Reach for Mediator when you can answer "yes" to any of these:
- N objects that talk to each other. Form widgets, chatroom participants, dashboard panels, microservice subscribers — anywhere "X happens, then Y, Z, and W react" and the reactions know about each other.
- The interaction rules are getting harder than the objects themselves. When the file with the most logic is the one wiring listeners together, those rules want their own home.
- You want to change interaction policy without rewriting widgets. Country/region dependency works one way today and a different way tomorrow — only the mediator changes.
- Tests need to assert "what happens when…" Mediator is one class with focused logic; testing it in isolation is straightforward.
If you only have two or three objects with one or two dependencies, leave them coupled. Mediator earns its keep when N starts climbing — typically four or more colleagues with rules that change.
Pros and Cons
Pros
- Colleagues don't import each other; they only know the mediator. Reduce coupling dramatically.
- Coordination rules live in one place — readable, testable, swappable.
- Adding a new colleague is a localized change: register it with the mediator, the mediator decides what events affect it.
- The same colleagues can be reused in different contexts by giving them a different mediator.
Cons
- The Mediator can become a god class. All the rules pile up; the file grows; the original promise of "loose coupling" is replaced by "tight coupling to the mediator." Watch for this and split when you must.
- Indirect control flow is harder to debug. "Why did this field hide?" requires reading the mediator, not the field.
- Adds a layer. For very small dependency graphs, the indirection costs more than it saves.
Pro Tips
- Keep the Mediator focused on coordination, not business logic. "Hide the VAT field if the country is EU" is coordination. "Calculate the VAT amount" is business logic — that belongs elsewhere.
- One Mediator per related group, not per application. A registration form gets its own mediator. The whole app shouldn't share one.
- Pass colleagues by interface, not concrete class. The mediator should reference
FieldandButton, notEmailFieldorSubmitButton— colleagues become easier to mock and swap. - When the Mediator gets too big, split by concern, not by colleague. A
ValidationCoordinatorand aVisibilityCoordinatorare cleaner than aRegistrationFormMediatorthat does both. - Use it when you'd otherwise reach for a chain of Observer subscriptions between widgets. Observer is one-way (subject → observers). Mediator is bidirectional — colleagues both send and receive through the same hub.
Relations with Other Patterns
- Observer is one-way: a subject notifies many observers, and observers don't push state back. Mediator is bidirectional: colleagues both push events to the mediator and receive updates from it. Many Mediators use Observer-style subscriptions internally to deliver notifications.
- Facade also offers a single point of contact, but only for outgoing calls (client → subsystem). Mediator coordinates between peer objects.
- Command can deliver the event a colleague sends to the Mediator, especially if you want events queued, logged, or replayed.
- Event Bus / Pub-Sub is Mediator generalized to anonymous colleagues — components don't know the mediator either, just publish and subscribe by topic. Same idea, different coupling profile.
Final Tips
The Mediator pattern is the fix for code where the bug report says, "when I change X, three other things go wrong, and one of them is a regression from when I fixed it last sprint." That sentence describes a system whose coordination rules are spread across a dozen files — and the only durable fix is to give those rules a single home.
Reach for Mediator when colleagues start importing each other. Resist when the colleagues are small in number and stable in their relationships. The pattern's promise isn't zero coupling — it's centralized coupling, in a file you can name and a class you can test.


