Mediator Pattern
The Mediator pattern introduces a central object that handles communication between a set of components, so the components stop talking to each other directly. Instead of every component holding a reference to every other component, each one holds a single reference to the mediator. The mediator knows who everyone is and decides where each message goes.
The payoff is decoupling: a component only needs to know how to talk to the mediator, not the identities, count, or routing rules of its peers. The cost is that the mediator concentrates that knowledge in one place — which is exactly the trade-off you have to manage.
The problem it solves
Imagine an office where HR, Finance, and Technical must share updates. If every department wires up direct links to every other department, the connections grow as n(n−1) — and every time you add a department you must touch all the existing ones. The Mediator replaces that mesh with a hub: each department connects only to the central office, which fans messages out.
The three roles
- Mediator interface — declares how components hand a message to the hub. Here:
route(message, sender). - Concrete mediator (
OfficeMediator) — holds references to every component and contains the routing logic. This is the one class that knows the whole topology. - Components (
HRDepartment,FinanceDepartment,TechnicalDepartment) — each holds only a reference to the mediator. A component sends by handing the message to the mediator and receives when the mediator calls back. Crucially, a component never names another component.
The design we implement below is a broadcast-except-sender mediator: when any department sends, the mediator delivers the message to every other registered department and skips the sender (no department hears its own announcement echoed back).
Runnable implementation (Java)
This compiles and runs as-is. main() registers exactly three departments — HR, Finance, and Technical, each once — wires them to the mediator, then HR broadcasts. Read the routing rule in OfficeMediator.route closely: it is sender exclusion, not target naming.
interface Mediator {
void route(String message, Department sender);
}
interface Department {
void setMediator(Mediator mediator);
void send(String message);
void receive(String message);
}
class HRDepartment implements Department {
private Mediator mediator;
public void setMediator(Mediator m) { this.mediator = m; }
public void send(String message) { mediator.route(message, this); }
public void receive(String message) {
System.out.println("HR received: " + message);
}
}
class FinanceDepartment implements Department {
private Mediator mediator;
public void setMediator(Mediator m) { this.mediator = m; }
public void send(String message) { mediator.route(message, this); }
public void receive(String message) {
System.out.println("Finance received: " + message);
}
}
class TechnicalDepartment implements Department {
private Mediator mediator;
public void setMediator(Mediator m) { this.mediator = m; }
public void send(String message) { mediator.route(message, this); }
public void receive(String message) {
System.out.println("Technical received: " + message);
}
}
class OfficeMediator implements Mediator {
private HRDepartment hr;
private FinanceDepartment finance;
private TechnicalDepartment technical;
public void setHR(HRDepartment d) { this.hr = d; }
public void setFinance(FinanceDepartment d) { this.finance = d; }
public void setTechnical(TechnicalDepartment d) { this.technical = d; }
// Broadcast to every registered department EXCEPT the sender.
public void route(String message, Department sender) {
if (sender != hr && hr != null) hr.receive(message);
if (sender != finance && finance != null) finance.receive(message);
if (sender != technical && technical != null) technical.receive(message);
}
}
public class Solution {
public static void main(String[] args) {
OfficeMediator mediator = new OfficeMediator();
HRDepartment hr = new HRDepartment();
FinanceDepartment finance = new FinanceDepartment();
TechnicalDepartment technical = new TechnicalDepartment();
mediator.setHR(hr);
mediator.setFinance(finance);
mediator.setTechnical(technical);
hr.setMediator(mediator);
finance.setMediator(mediator);
technical.setMediator(mediator);
hr.send("HR update for all departments");
}
}Compiled and run, this prints exactly:
Finance received: HR update for all departments
Technical received: HR update for all departmentsWorked trace of hr.send("HR update for all departments")
Follow the single call through the system. The sender is the HR object; the mediator routes by identity of that sender object, and HR is excluded from delivery because it is the sender.
| Step | Who runs | Call | Effect |
|---|---|---|---|
| 1 | hr | send(msg) | HR does not know Finance or Technical exist. It just hands the message to its mediator: mediator.route(msg, this). |
| 2 | OfficeMediator | route(msg, hr) | Evaluate sender != hr → false. HR is the sender, so it is skipped — no echo back. |
| 3 | OfficeMediator | finance.receive(msg) | sender != finance → true and finance != null → true, so Finance is delivered to. Prints Finance received: … |
| 4 | OfficeMediator | technical.receive(msg) | sender != technical → true and technical != null → true, so Technical is delivered to. Prints Technical received: … |
The message reached Finance and Technical, while HR never named either of them — the mediator owns that routing. This trace is exactly what the program prints above.
Why route by identity — and why == is correct here
The mediator decides delivery by asking "is this the same object I have stored as hr?" In Java, == on references answers exactly that: same object or not. Because none of the department classes overrides equals(), the inherited Object.equals() is already reference identity — so == and .equals() would behave identically here. We use == deliberately, to make it explicit that routing is keyed on object identity, and the demo never relies on creating two "equal-but-different" objects to make the output come out right.
This matters because the wrong instinct is to route on a value like a department name string. If you did if (sender.getName().equals("HR")), two departments that happened to share a name would be indistinguishable, and the mediator would have to know about names — leaking domain logic into routing. Identity routing keeps the mediator agnostic: it just compares against the references it was handed at registration.
Adding a fourth department is one registration, not a rewrite
This is the concrete payoff of the hub. To add a Legal department, the existing departments stay completely untouched — they already know only the mediator. You add the new class, give the mediator one field + setter, extend route by one line, and register it once in main():
class LegalDepartment implements Department {
private Mediator mediator;
public void setMediator(Mediator m) { this.mediator = m; }
public void send(String message) { mediator.route(message, this); }
public void receive(String message) {
System.out.println("Legal received: " + message);
}
}
// in OfficeMediator:
private LegalDepartment legal;
public void setLegal(LegalDepartment d) { this.legal = d; }
// add inside route(...):
if (sender != legal && legal != null) legal.receive(message);
// in main(): register it once
LegalDepartment legal = new LegalDepartment();
mediator.setLegal(legal);
legal.setMediator(mediator);In the direct-mesh design, the same change would force you to edit HR, Finance, and Technical to teach each of them about Legal. The mediator localizes that churn to one class.
Mediator vs. Observer — they are not the same thing
Both decouple a sender from its receivers through an intermediary, so they are easy to confuse. The difference is in who owns the routing logic and what the intermediary knows.
| Mediator | Observer | |
|---|---|---|
| Intermediary's knowledge | Knows the concrete participants and contains conditional routing — "if sender is HR, deliver to Finance and Technical." | The subject knows only "a list of observers." It has no per-observer logic; it blindly notifies all subscribers. |
| Direction | Many-to-many. Any participant can send; the mediator decides each route. | One-to-many. One subject broadcasts state changes outward to its subscribers. |
| Where logic lives | Centralized in the mediator — it is the "smart" object. | Spread into each observer's update() — the subject stays "dumb." |
| Coupling style | Participants couple to a hub that orchestrates them. | Subscribers couple to an event/topic, not to each other or to the publisher's logic. |
Rule of thumb: reach for Observer / pub-sub / an event bus when you want "announce a change, and whoever cares reacts" with no central brain. Reach for Mediator when the interaction itself has rules — coordination, ordering, who-talks-to-whom — that you want to capture in one place. Our route method has exactly that kind of rule (sender exclusion), which is why it is a mediator and not a plain broadcast.
When to use it — and when not to
The central pitfall of Mediator is the god object: as you push more coordination into the mediator, it accumulates every rule about every component and becomes a tangled, hard-to-test bottleneck. Turn that pitfall into a selection rule:
Reach for Mediator when
- Several components must interact in many-to-many ways and the wiring is becoming an n² mesh.
- The interaction has rules (ordering, filtering, sender exclusion, validation) you want to read in one place.
- You expect to add or remove components often and want that change localized.
Prefer direct coupling when
- There are only two or three components and the relationships are stable. A mediator here is ceremony that hides a trivial call behind indirection.
- The call is a simple A-tells-B with no routing decision to make.
Prefer Observer / pub-sub when
- You want fan-out "notify everyone interested" with no coordinating logic — subscribers decide independently how to react.
- Publishers and subscribers should not even know the hub's routing rules, only a topic/event.
Watch for the god-object smell
- If the mediator's routing method keeps growing conditionals and now encodes business rules that belong to the components, the mediator has become the coupling it was meant to remove. Split it, or move logic back into the components, before it calcifies.
Re-entrancy: a reply arrives mid-dispatch
The demo's route touches three hard-coded fields, but the moment a mediator holds its colleagues in a list (the general form once there are more than a handful) it loops over that list and calls receive on each. That opens the single hardest Mediator follow-up a senior interviewer will ask: what happens if a colleague's receive() synchronously calls send() (hence route()) to post an immediate reply? That call re-enters route() while the outer loop over the same colleague list is still mid-flight, lower on the call stack. Depending on how naively the mediator is written, one of three failure shapes appears:
- ConcurrentModificationException — if the reply causes a colleague to register/unregister while the outer loop is still iterating the same backing list.
- Double-notify — a colleague notified twice for one logical event, if the re-entrant call re-starts iteration from the top.
- Unbounded recursion — if the replied-to colleague replies back, the two volley forever, each call adding a stack frame until it overflows.
Traced: HR's budget request triggers a reply that itself replies
Suppose Finance.receive(BudgetRequest) immediately calls mediator.route(approvalReply, finance), and (a genuine, if sloppy, bug) HR.receive(ApprovalReply) in turn sends an acknowledgement back. Every frame below is still open — none has returned — so the depth-0 loop is frozen mid-iteration for the whole nested storm:
| Call-stack depth | Frame | What it is doing |
|---|---|---|
| 0 | route(BudgetRequest, hr) | outer loop, currently delivering to Finance |
| 1 | finance.receive(BudgetRequest) | calls route(ApprovalReply, finance) before returning |
| 2 | route(ApprovalReply, finance) | a brand-new loop over the SAME colleague list, re-entered |
| 3 | hr.receive(ApprovalReply) | calls route(AckReply, hr) before returning |
| 4 | route(AckReply, hr) | another re-entrant loop; if the volley continues, depth 5, 6, 7… never unwinds |
Fix: queue instead of recurse, and snapshot the colleague list
Treat the mediator as a single-threaded event loop. While a dispatch is in progress, a nested route() call does not recurse — it enqueues the message and returns immediately, and the original loop drains the queue, one message to completion at a time, after its own iteration finishes:
public class OfficeMediator implements Mediator {
private final List<Department> colleagues = new ArrayList<>();
private record Pending(String message, Department sender) {}
private final Deque<Pending> pending = new ArrayDeque<>();
private boolean dispatching = false;
public void route(String message, Department sender) {
pending.addLast(new Pending(message, sender));
if (dispatching) return; // re-entrant call: queue it, DO NOT recurse
dispatching = true;
try {
while (!pending.isEmpty()) {
Pending p = pending.pollFirst();
for (Department d : List.copyOf(colleagues)) { // snapshot: safe against
if (d != p.sender()) d.receive(p.message()); // (un)registration mid-loop
}
}
} finally {
dispatching = false;
}
}
}Two distinct fixes are stacked here and both matter. The dispatching flag + pending queue stops re-entrant recursion — the reply storm now runs as a flat sequence of drained messages instead of a growing call stack. The snapshot copy of the colleague list (List.copyOf) is a separate defence: if the colleague list is mutated during the dispatch loop — a department registering or unregistering as a side effect of a receive() — iterating the live list would throw ConcurrentModificationException or silently skip a colleague, so the loop walks a snapshot taken at the top of each drain instead. Fixing only one leaves the other failure mode live. This is the same discipline worked in full on the guide's Behavioral Patterns — Senior Gotchas & Follow-ups deep dive.
Source
Adapted and corrected from the Knowledge Guide lesson Mediator Pattern (OO & Low-Level Design → Behavioral). The runnable Java was compiled and executed to confirm its output (Finance received… / Technical received…); the worked trace, sequence diagram, and prose were rewritten to agree with that verified behavior. Pattern definition and the Mediator-versus-Observer distinction follow the Gang of Four, Design Patterns: Elements of Reusable Object-Oriented Software (1994).
🤖 Don't fully get this? Learn it with Claude
Stuck on Mediator Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Mediator Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Mediator Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Mediator Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Mediator Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Mediator Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.