Mediator pattern
Open in the interactive app: practice, quiz, mock interview →In one minute
- Mediator stops a group of objects from talking to each other directly. Each one knows only the mediator; when something happens it tells the mediator, and the mediator decides who else must react and how.
- It turns a mesh of up to N(N-1)/2 pairwise links into a star of N links, and it puts the interaction rules ("changing the country resets the region and may remove express shipping") in one readable place instead of scattering them across event handlers.
- Unlike Observer, which is only a notification mechanism, a mediator holds policy. Unlike a Facade, the parts know the mediator and call into it.
- The risk is a god object: every new interaction lands in the mediator until it is the hardest file in the codebase. Split mediators by interaction cluster and keep components dumb.
- The classic bugs are cascades. A mediator that updates a component, which notifies the mediator, which updates another component, can loop or apply rules twice. Mediator-initiated updates must not re-enter the mediator.
How it works
The problem
A checkout form has four controls: a country select, a region select, a shipping-method select and a "terms accepted" checkbox, plus a Place order button. The rules are simple to state. Region options depend on the country, and the region is cleared when the country changes. Some countries have no regions. Express shipping exists only in some countries. The button is enabled only when everything required is filled in.
The first implementation wires the controls to each other. The country handler clears the region, reloads its options, filters shipping and re-checks the button; the other three handlers each re-check the button too. Every control holds references to three or four others, the "is the form complete?" logic is copied four times, and the copies disagree. A new promo code field that affects shipping means hunting for every handler that should consider it.
Put the rules in the middle
With a mediator, each control knows one thing: whom to tell when its value changes.
class Widget:
def set(self, value):
self._check(value) # disabled? valid option?
if value != self.value:
self.value = value
self.mediator.changed(self)The form object is the mediator. Its changed(widget) method is the only place that knows the rules: if the sender is the country, reset the region, load its options and filter shipping; in every case, recompute whether the button is enabled. Controls are now reusable (the same select works on the address book page with a different mediator) and testable alone. The rules can be read top to bottom in one method.
Mediator, Observer, Facade, event bus
These are easy to confuse because they all reduce direct references.
- Observer is a mechanism: a subject broadcasts, observers react, and the subject has no opinion about what they do. A mediator often uses observer-style notifications to hear from components, but it adds policy about what happens next.
- Facade (see that lesson) sits in front of a subsystem whose parts do not know it exists; callers go through it. In a mediated group, the parts know the mediator and talk to each other through it.
- An event bus routes messages by topic without knowing the participants or the rules. It decouples more and explains less: the interaction logic is spread across subscribers again, just without imports.
A useful test: if you can point to one place and say "this is where the rules about how these components interact live", you have a mediator.
Where you already use it
- React's "lifting state up". Two sibling components that must agree do not reference each other; their common parent owns the state and passes values and callbacks down. The parent is the mediator.
- Kubernetes controllers. The Deployment controller does not call the ReplicaSet controller, and neither calls the scheduler. Each reads and writes objects through the API server and watches for changes, so the API server and its stored state mediate every interaction between them.
- Exchanges and air traffic control. Buyers and sellers never contact each other, and aircraft do not negotiate runways; the matching engine and the tower decide.
Keeping the mediator from becoming a god object
A mediator absorbs complexity on purpose, so it grows. Three habits keep it healthy. First, keep components ignorant: they report what happened ("value changed") and never what should happen next ("please reset the region"). Second, split by cluster: an address section mediator and a payment section mediator, each owning their rules, with a thin form-level mediator joining them. Third, when rules become numerous and regular, express them as data (a dependency table: "region depends on country; shipping depends on country and weight") and let the mediator evaluate the table.
Reviewing agent-written code
Asked to "clear the region when the country changes", an agent will add regionSelect.clear() to the country's change handler: one more direct link. Asked for something more decoupled, it often reaches for a global event bus with string topics, which hides the links instead of removing them. In review, ask where the interaction rules live and whether a component imports or calls a sibling. Check for re-entrancy: does setting a value from inside the mediator trigger the mediator again? Check that the "is it complete?" logic exists once.
The same shape elsewhere
A mediator is a contention and ordering answer: every interaction goes through one point, so interactions are serialised and applied in a known order. The same trade appears in distributed systems as coordinator versus peer-to-peer designs. A coordinator (a leader, a lock service like etcd, an orchestrator for a saga) is easy to reason about and becomes a bottleneck and a single point of failure; gossip and choreography avoid the bottleneck and make the global behaviour harder to see. The choice inside a form and the choice across 40 services follow the same reasoning.
Down the stack
Layer 1The contract
Components promise to report their own changes to the mediator and to never reference sibling components. The mediator promises that after it finishes handling any notification, the group's invariants hold: the region is valid for the country, shipping is valid for the country, and the button is enabled exactly when the form is complete. It also promises that its own updates to components do not generate new notifications, so handling one user action runs the rules once. Handling is synchronous and in a defined order, so a test can set a value and immediately assert the whole group's state.
Layer 2The mechanism
The structure is a star: the mediator holds typed references to each component, and each component holds one reference to the mediator, usually through a small interface (changed(sender)) so components do not depend on the concrete form. Re-entrancy is handled in one of two ways. Either components expose a silent setter that the mediator uses (the practice solution's approach), or the mediator sets an "updating" flag and ignores notifications that arrive while it is applying rules.
In React, the parent's state setter schedules a re-render rather than mutating children; React 18 batches the updates from one event into one render, and children receive new props. In Kubernetes, controllers use informers that list and watch resources into a local cache and queue the keys that changed; reconciliation writes through the API server with optimistic concurrency, where a stale resourceVersion fails with a conflict and the controller retries. The API server never calls controllers; the interaction emerges from shared state. In Go programs, a common in-process mediator is a single goroutine that owns the state and serves requests from channels in a select loop, so no lock is needed because only one goroutine touches the data.
Layer 3The machine
The star costs N references instead of up to N(N-1)/2, and each interaction costs one indirect call into the mediator plus the rule evaluation, a few nanoseconds in Go and well under a microsecond in a browser. The memory effect worth knowing is cycles: components point to the mediator and the mediator points to components. Tracing collectors (V8, the JVM, Go) reclaim such cycles once nothing outside reaches them, but CPython frees most objects by reference counting and needs its cyclic collector to reclaim them, which runs later. A long-lived global mediator, like a lapsed observer, keeps every registered component alive until it is explicitly removed.
Concurrency is where the mediator's centrality shows. Because every interaction passes through it, it is a natural serialisation point: one mutex or one owner goroutine. That makes it correct by construction and limits throughput to one interaction at a time. If each interaction holds the lock for 20 microseconds, the ceiling is 50,000 interactions per second no matter how many cores there are. An uncontended Go channel send and receive costs on the order of 100 ns, cheap in a form and noticeable in a market data path. When the ceiling bites, partition: one mediator per order book, per chat room, per document, so unrelated interactions do not contend.
Why it's built this way
- Components that react to each other directly need links that grow quadratically with their number.
- Interaction rules scattered across handlers disagree and cannot be read as a whole, so they move to one object.
- Components then know only that object, which makes them reusable and testable alone.
- One object applying all rules serialises interactions, which makes the rules easy to reason about and makes the mediator a bottleneck and a growth hotspot.
- So mediators are kept per cluster or per partition, and kept free of re-entrant cascades.
Diagram
sequenceDiagram
participant U as User
participant C as Country select
participant M as Form mediator
participant R as Region select
participant S as Shipping select
participant B as Order button
U->>C: set("ID")
C->>M: changed(country)
M->>R: load options, clear value (silent)
M->>S: options [standard], drop "express" (silent)
M->>B: enabled = false
Note over M: one notification, rules applied onceWorked example
A travel site's booking page had 14 interactive controls (dates, travellers, cabin, fare family, seats, bags, insurance, promo code and others). An audit counted 31 change handlers reaching into other controls and 6 different copies of the "can the user continue?" check. Over two quarters, 9 production bugs came from these interactions, such as a promo code unlocking a fare family whose bag allowance was not re-applied when travellers changed.
The rewrite made every control report only its own changes, to a trip-section mediator or an extras-section mediator, joined by a page mediator that owns the continue check. The dependency rules became a table of 22 entries such as "bags depends on fare family and travellers", and sibling references per control fell from 3.2 on average to zero.
Interaction bugs fell to 1 in the next two quarters. Rule handling took about 0.3 ms per user action against a 16 ms frame budget, and a change that once touched 7 handlers became one table row and one test.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Direct references between components | Obvious for two or three parts | Quadratic links; rules scattered and duplicated | Two components with one interaction |
| Observer between components | No imports between parts | Rules still spread across subscribers; cascades hard to see | One source, many independent reactions |
| Global event bus | Maximum decoupling; plugins can join | Rules invisible; string topics; ordering surprises | Cross-module notifications without shared policy |
| Mediator object | One place for rules; components reusable | Can grow into a god object; serialisation point | A cluster of components with real interaction rules |
| Central state store (Redux style) | State and transitions explicit and replayable | Boilerplate; everything global | Large apps where many views share state |
Pitfalls in production
- Cascading notifications: the mediator updates the region, the region notifies, the rules run again. Fix: silent setters for mediator updates, or a re-entrancy flag, plus a test that one user action runs the rules once.
- The mediator becomes the place for all logic, including validation and formatting that belong in components. Fix: keep components responsible for their own validity; the mediator only coordinates.
- Components that "help" by calling siblings directly, reintroducing the mesh. Fix: no component may import another; enforce with a lint rule or package boundaries.
- A single global mediator for the whole app, creating contention and leaks. Fix: one mediator per screen, room, document or partition, with explicit teardown.
- Rules that depend on the order of notifications from asynchronous sources (two API responses arriving in either order). Fix: have the mediator recompute from current state rather than applying deltas, so order does not matter.
Leadership lens
- Ask in design review: "Where do the rules about how these parts interact live?" If the answer is "in several handlers", ask for a mediator; if it is "in a 2,000-line mediator", ask for a split.
- Treat the mediator as a team boundary. A form section owned by one team can have its own mediator; cross-team interactions go through a thin, reviewed top-level mediator.
- Watch the serialisation point in backend mediators. Know the per-interaction cost and the partition key before it becomes a hotspot.
- Coordinator versus choreography is the same decision at system scale. A team that understands why a form uses a mediator will make better calls on sagas and orchestration.
- For agents, "components only report changes; all cross-component rules go in the mediator" is a precise instruction that a reviewer can check with a grep for sibling imports.
Check yourself
How does a mediator differ from an observer?
Show answer
Observer is a broadcast mechanism with no policy about what reactions do. A mediator receives notifications and decides, by explicit rules, which other components change and how.
Why must updates made by the mediator not notify the mediator again?
Show answer
Otherwise one user action triggers the rules repeatedly or recursively, which wastes work, can apply resets twice, and can loop forever when rules depend on each other.
A chat server uses one mediator object for all rooms and slows down at peak. What do you change?
Show answer
Partition the mediator by room, so unrelated rooms do not contend on the same lock or goroutine, and keep any cross-room coordination separate and small.
In what sense is the Kubernetes API server a mediator?
Show answer
Controllers never call each other; they watch and write shared objects through the API server, so all coordination between them happens through that one component and its stored state.
Watch
- Mediator Design Pattern (Derek Banas): a code walkthrough of components talking only through a mediator.
- Mediator, Design Patterns in 5 minutes (levonog): a short visual summary of the star structure.
- Javascript Design Patterns: Mediator Pattern (DevSage): the pattern in JavaScript, close to how UI code uses it.
Free resources
- Mediator (Refactoring.Guru): the canonical structure with a form dialog example and comparisons with Observer and Facade.
- Sharing State Between Components (React docs): lifting state up, the mediator idiom in React.
- Controllers (Kubernetes docs): controllers that coordinate through the API server rather than with each other.
- Share Memory By Communicating (Go blog): a single goroutine owning state and serving requests over channels.
- Mediator pattern (Wikipedia): background and the original Gang of Four framing.