CMD Guide
HomeOO & Low-Level DesignBehavioral

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.

diagram
diagram

The three roles

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 departments

Worked 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.

StepWho runsCallEffect
1hrsend(msg)HR does not know Finance or Technical exist. It just hands the message to its mediator: mediator.route(msg, this).
2OfficeMediatorroute(msg, hr)Evaluate sender != hrfalse. HR is the sender, so it is skipped — no echo back.
3OfficeMediatorfinance.receive(msg)sender != finance → true and finance != null → true, so Finance is delivered to. Prints Finance received: …
4OfficeMediatortechnical.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.

diagram
diagram

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.

MediatorObserver
Intermediary's knowledgeKnows 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.
DirectionMany-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 livesCentralized in the mediator — it is the "smart" object.Spread into each observer's update() — the subject stays "dumb."
Coupling styleParticipants 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

Prefer direct coupling when

Prefer Observer / pub-sub when

Watch for the god-object smell

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:

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 depthFrameWhat it is doing
0route(BudgetRequest, hr)outer loop, currently delivering to Finance
1finance.receive(BudgetRequest)calls route(ApprovalReply, finance) before returning
2route(ApprovalReply, finance)a brand-new loop over the SAME colleague list, re-entered
3hr.receive(ApprovalReply)calls route(AckReply, hr) before returning
4route(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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes