InterviewKata
Design patterns · 19 of 26 · Objects talking

Mediator

A mediator takes the coordination between a group of objects and puts it in one place, so the objects report to it instead of calling each other. This page builds a station controller for Pedal's docks, display, kiosk and operations alerts, and counts the links it removes.

1. The pain

A Pedal station is a row of docks plus a display showing free bikes and docks, a kiosk screen that talks to riders, and an alert line to the operations team. When a dock jams, the rider must be sent to another free dock, the display must change, and if the station is now full, operations needs a van.

In the first version each dock did all of that itself: it asked its neighbours which were free, told the kiosk, updated the display and messaged operations. Every dock knew every other part. When the kiosk's interface changed, every dock class changed; when the redirect rule changed ("send riders to the nearest free dock, not the next one"), the logic was spread across all the docks.

2. The idea in one picture

At an airport, planes don't negotiate with each other about who lands next. Each pilot talks to the control tower, and the tower, which sees everything, tells each plane what to do. Add a runway or a rule and you retrain the tower, not every pilot.

Mediator puts the coordination between a group of objects (the colleagues) into one object that they all talk to instead of to each other. Docks just report what happened to them: controller.notify(self, "jammed"). The StationController decides who else needs to know and what to tell them: the kiosk redirects the rider, the display updates, operations is alerted when the station fills.

Links drop from "everyone to everyone" to "everyone to the hub". The price is that the hub holds all the coordination rules, so it must be watched so that it doesn't become a god object.

3. The roles

RoleIts jobIn Pedal
MediatorKnows the colleagues and the rules for how they interact.StationController.notify(sender, event)
ColleagueReports its own events to the mediator; never calls other colleagues.Dock
Other colleaguesReceive instructions from the mediator.Display, Kiosk, OpsAlerts

4. Before and after

A dock jam both ways (Python sketch; the full program is in section 5).

Without the pattern

class Dock:
    def jam(self):
        self.jammed = True
        nxt = next(d for d in self.neighbours
                   if d.is_free())
        self.kiosk.say(f"use {nxt.dock_id}")
        self.display.show(self.count_bikes(),
                          self.count_free())
        if not any(d.is_free()
                   for d in self.neighbours):
            self.ops.send("station full")

With it

class Dock:
    def jam(self):
        self.jammed = True
        self.controller.notify(self, "jammed")

class StationController:
    def notify(self, sender, event):
        # the only place that knows the rules
        # and the other parts
        ...
The next changeWithout the patternWith it
Redirect riders to the nearest free dockEdit the jam logic in every dockEdit StationController.notify
The kiosk's interface changesEvery dock class changesThe controller changes
Test a dockSet up neighbours, a kiosk, a display and opsA fake controller that records notifications

The colleagues become simple and reusable; the rules about how they work together live in one place.

5. The code

Docks only call controller.notify(). The controller redirects riders when D2 jams, keeps the display right, and alerts operations when D4 fills the station.

from __future__ import annotations


# ---- Colleagues: each talks only to the controller, never to each other. ----
class Dock:
    def __init__(self, dock_id: str, occupied: bool, controller: StationController) -> None:
        self.dock_id, self.occupied, self.jammed = dock_id, occupied, False
        self.controller = controller

    def bike_returned(self) -> str:
        self.occupied = True
        return self.controller.notify(self, "returned")

    def jam(self) -> str:
        self.jammed = True
        return self.controller.notify(self, "jammed")


class Display:
    def show(self, bikes: int, free: int) -> str:
        return f"display: {bikes} bikes, {free} free dock{'s' if free != 1 else ''}"


class Kiosk:
    def say(self, text: str) -> str:
        return f"kiosk: {text}"


class OpsAlerts:
    def send(self, text: str) -> str:
        return f"ops: {text}"


# ---- The mediator: the only object that knows how the others fit together. ----
class StationController:
    def __init__(self, name: str) -> None:
        self.name = name
        self.docks: list[Dock] = []
        self.display, self.kiosk, self.ops = Display(), Kiosk(), OpsAlerts()

    def add_docks(self, occupied: list[bool]) -> None:
        self.docks = [Dock(f"D{i + 1}", o, self) for i, o in enumerate(occupied)]

    def notify(self, sender: Dock, event: str) -> str:
        bikes = sum(d.occupied for d in self.docks)
        free = [d for d in self.docks if not d.occupied and not d.jammed]
        out = [f"{sender.dock_id} {'returned a bike' if event == 'returned' else 'jammed'} ->"]
        if event == "jammed":
            out.append(self.kiosk.say(f"{sender.dock_id} out of order, use {free[0].dock_id}" if free else f"{sender.dock_id} out of order, station full") + " |")
        out.append(self.display.show(bikes, len(free)))
        if not free:
            out.append("| " + self.ops.send(f"{self.name} full, send a van"))
        return " ".join(out)


if __name__ == "__main__":
    station = StationController("S-31")
    station.add_docks([False, False, False, False, True, True, True, True])
    d = {dock.dock_id: dock for dock in station.docks}
    print(d["D1"].bike_returned())
    print(d["D2"].jam())
    print(d["D3"].bike_returned())
    print(d["D4"].bike_returned())
// ---- Colleagues: each talks only to the controller, never to each other. ----
class Dock {
  jammed = false;
  constructor(readonly dockId: string, public occupied: boolean, private readonly controller: StationController) {}
  bikeReturned(): string { this.occupied = true; return this.controller.notify(this, "returned"); }
  jam(): string { this.jammed = true; return this.controller.notify(this, "jammed"); }
}

class Display { show(bikes: number, free: number) { return `display: ${bikes} bikes, ${free} free dock${free !== 1 ? "s" : ""}`; } }
class Kiosk { say(text: string) { return `kiosk: ${text}`; } }
class OpsAlerts { send(text: string) { return `ops: ${text}`; } }

// ---- The mediator: the only object that knows how the others fit together. ----
class StationController {
  docks: Dock[] = [];
  private readonly display = new Display();
  private readonly kiosk = new Kiosk();
  private readonly ops = new OpsAlerts();
  constructor(private readonly name: string) {}

  addDocks(occupied: boolean[]): void { this.docks = occupied.map((o, i) => new Dock(`D${i + 1}`, o, this)); }

  notify(sender: Dock, event: "returned" | "jammed"): string {
    const bikes = this.docks.filter((d) => d.occupied).length;
    const free = this.docks.filter((d) => !d.occupied && !d.jammed);
    const out = [`${sender.dockId} ${event === "returned" ? "returned a bike" : "jammed"} ->`];
    if (event === "jammed") out.push(this.kiosk.say(free.length ? `${sender.dockId} out of order, use ${free[0].dockId}` : `${sender.dockId} out of order, station full`) + " |");
    out.push(this.display.show(bikes, free.length));
    if (!free.length) out.push("| " + this.ops.send(`${this.name} full, send a van`));
    return out.join(" ");
  }
}

const station = new StationController("S-31");
station.addDocks([false, false, false, false, true, true, true, true]);
const d = Object.fromEntries(station.docks.map((x) => [x.dockId, x]));
console.log(d.D1.bikeReturned());
console.log(d.D2.jam());
console.log(d.D3.bikeReturned());
console.log(d.D4.bikeReturned());
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    // ---- Colleagues: each talks only to the controller, never to each other. ----
    static class Dock {
        final String dockId;
        boolean occupied, jammed;
        private final StationController controller;
        Dock(String dockId, boolean occupied, StationController controller) { this.dockId = dockId; this.occupied = occupied; this.controller = controller; }
        String bikeReturned() { occupied = true; return controller.notify(this, "returned"); }
        String jam() { jammed = true; return controller.notify(this, "jammed"); }
    }

    static class Display { String show(int bikes, int free) { return "display: " + bikes + " bikes, " + free + " free dock" + (free != 1 ? "s" : ""); } }
    static class Kiosk { String say(String text) { return "kiosk: " + text; } }
    static class OpsAlerts { String send(String text) { return "ops: " + text; } }

    // ---- The mediator: the only object that knows how the others fit together. ----
    static class StationController {
        private final String name;
        final List<Dock> docks = new ArrayList<>();
        private final Display display = new Display();
        private final Kiosk kiosk = new Kiosk();
        private final OpsAlerts ops = new OpsAlerts();
        StationController(String name) { this.name = name; }

        void addDocks(boolean[] occupied) { for (int i = 0; i < occupied.length; i++) docks.add(new Dock("D" + (i + 1), occupied[i], this)); }

        String notify(Dock sender, String event) {
            int bikes = (int) docks.stream().filter(x -> x.occupied).count();
            List<Dock> free = docks.stream().filter(x -> !x.occupied && !x.jammed).toList();
            List<String> out = new ArrayList<>();
            out.add(sender.dockId + " " + (event.equals("returned") ? "returned a bike" : "jammed") + " ->");
            if (event.equals("jammed"))
                out.add(kiosk.say(free.isEmpty() ? sender.dockId + " out of order, station full" : sender.dockId + " out of order, use " + free.get(0).dockId) + " |");
            out.add(display.show(bikes, free.size()));
            if (free.isEmpty()) out.add("| " + ops.send(name + " full, send a van"));
            return String.join(" ", out);
        }
    }

    public static void main(String[] args) {
        StationController station = new StationController("S-31");
        station.addDocks(new boolean[] {false, false, false, false, true, true, true, true});
        Map<String, Dock> d = new HashMap<>();
        for (Dock x : station.docks) d.put(x.dockId, x);
        System.out.println(d.get("D1").bikeReturned());
        System.out.println(d.get("D2").jam());
        System.out.println(d.get("D3").bikeReturned());
        System.out.println(d.get("D4").bikeReturned());
    }
}

Output (the same in all three languages)

D1 returned a bike -> display: 5 bikes, 3 free docks
D2 jammed -> kiosk: D2 out of order, use D3 | display: 5 bikes, 2 free docks
D3 returned a bike -> display: 6 bikes, 1 free dock
D4 returned a bike -> display: 7 bikes, 0 free docks | ops: S-31 full, send a van

6. Try it

Count the connections

Each dock may need to talk to the display, the kiosk, operations, and other docks. Grow the station.

Docks at the station
Without a mediator
With a station controller

Without a mediator, the worst case is that any part may need to reach any other part.

7. When not to use it

What you get

  • Fewer connections: n links to a hub instead of up to n(n−1)/2.
  • Simple colleagues: a dock only reports its own events.
  • Rules in one place: redirects and alerts are easy to find and change.
  • Reusable parts: docks don't know which station they're in.

What you pay

  • A hub that grows: every new rule lands in the mediator.
  • A single point of failure in the design: a bug in the controller affects the whole station.
  • Indirection: a dock's effect is found by reading the controller.
  • Overkill for two parts: two objects can just talk.
If…Use instead
Others only need to be told, with no coordination18 Observer
Callers outside need a simpler interface to a subsystem08 Facade
The coordination spans services and must be durableAn orchestrator or saga (System design track)
Only two or three objects interact in fixed waysDirect calls

8. How it goes wrong

The god mediatorA controller that also prices rides and schedules repairs is doing more than coordinating. Keep it to interaction rules.
Colleagues still calling each otherOne dock that still asks its neighbour directly reintroduces the web. All coordination goes through the mediator.
Business data inside the mediatorIf the controller stores the bikes' state, two sources of truth appear. Ask the colleagues; keep the controller about rules.
Confusing it with facadeA facade simplifies a subsystem for outsiders; a mediator is used by the parts themselves to talk to each other.

9. Remember it and say it

Memory hook: "Talk to the tower"Planes don't negotiate with each other; each reports to the tower, which tells the others.
How this hook works: what “Talk to the tower” means

What the slogan means, phrase by phrase

“Talk”
Colleagues report events.
“to the tower”
The mediator: StationController.
“tells the others”
The mediator decides who needs to know.

Why this picture fits: Air traffic control is coordination you can't do pairwise: too many planes, too many rules.

The story → the concept

The airportThe code
The control towerStationController
PlanesDocks
Ground crew and gatesDisplay, Kiosk, OpsAlerts
A new runway rule"Nearest free dock" redirect

Watch it on an example

  1. D2 jams and reports it to the controller.
  2. The controller finds D3 free, tells the kiosk to redirect, and updates the display.
  3. When D4 fills the last dock, the controller alerts operations.

The trap it saves you from

Every part knowing and calling every other part.

It still works when…

Yes: chat rooms, form dialogs where fields enable each other, and air traffic control itself.

In an interview or code review

10. Check yourself

Who do colleagues talk to in a mediator design?

That's what removes the web of links.

D2 jams. Who decides to send riders to D3?

The rules live in the mediator.

The kiosk's interface changes. What changes?

Only the mediator knows the kiosk.

What's the main risk of a mediator?

Keep it to coordination.

Mediator vs facade?

Different audiences.

11. Go deeper

ResourceWhy
18 ObserverBroadcast without coordination.
08 FacadeThe outsider-facing look-alike.
System design trackOrchestration and sagas: mediators across services.
Wikipedia: Mediator patternStructure and examples.