PideYa's logistics is a three-way conversation that never stops: the kitchen finishes an order and looks for a courier; the courier completes a delivery and asks if anything is pending; an order has been waiting too long and must be reassigned. In the current code, kitchen, orders, and couriers know and call each other: a mesh where every class holds references to the others, and where adding a piece (here come the scooter couriers with cooler boxes!) forces you to touch everything. Mediator proposes redoing the topology: no colleague talks to another directly, and all coordination flows through one central object — turning the mesh into a star. And with the usual honesty: we'll also see how that star can degenerate into the dreaded god mediator.
Contents
- The problem in PideYa: the delivery mesh
- Mesh vs. star: the coupling arithmetic
- Intent and structure of the pattern
- Complete Java implementation:
DispatchCenter - The conversation over time
- Variants of the pattern
- The god-mediator risk
- When to use it and when not to
- Relationship with other patterns
- Common mistakes
- Exercises and conclusion
The problem in PideYa: the delivery mesh
The current state, summed up in three classes locked in an embrace:
public class Kitchen {
private List<Courier> couriers; // knows the couriers
public void orderFinished(Order order) {
for (Courier c : couriers) { // and walks them itself
if (c.isAvailable() && c.acceptsZone(order.getZone())) {
c.assign(order);
return;
}
}
order.markWaitingForCourier(); // and decides what happens if there's none
}
}
public class Courier {
private Kitchen kitchen; // knows the kitchen
public void deliveryCompleted() {
this.available = true;
Order pending = kitchen.anyOrderWaiting(this.zone); // and asks it
if (pending != null) {
assign(pending);
}
}
}The symptoms, one by one:
- Everyone knows everyone:
Kitchenholds couriers,Courierholds the kitchen, and the stuck-order monitor will know both. Each cross-reference is a compile-time dependency and anew/setter someone maintains. - The coordination logic is scattered: part of "how a courier gets assigned" lives in
Kitchen, part inCourier. To understand the full protocol you must read every class; to change it, touch them all. - Reuse is impossible: a
Couriercan't be tested or reused without a realKitchenhanging off it. - Growth hurts quadratically: adding the congestion monitor, or external couriers from a subcontractor, means new cross-references in the existing classes.
And note what the problem is not: each class does its individual job well (cooking, delivering). What's excessive is that they also carry the coordination protocol. That protocol needs a single owner.
Mesh vs. star: the coupling arithmetic
With n participants all collaborating with all, the mesh has up to n(n−1)/2 connections; the star, exactly n. With 4 colleagues: 6 versus 4; with 8: 28 versus 8. But more important than the count is who knows what: in the star, each colleague knows only the mediator — adding the fifth colleague touches none of the other four.
flowchart LR
subgraph Mesh["Mesh: everyone knows everyone"]
C1[Kitchen] --- R1[Couriers]
C1 --- P1[Ready orders]
C1 --- M1[Congestion monitor]
R1 --- P1
R1 --- M1
P1 --- M1
end
subgraph Star["Star: everyone knows the mediator"]
C2[Kitchen] --- X[DispatchCenter]
R2[Couriers] --- X
P2[Ready orders] --- X
M2[Congestion monitor] --- X
end
Intent and structure of the pattern
Intent (GoF): define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
classDiagram
class DispatchMediator {
<<interface>>
+orderReady(order: Order)
+courierAvailable(c: Courier)
+orderStuck(order: Order)
}
class DispatchCenter {
-waiting: Queue~Order~
-available: List~Courier~
+orderReady(order: Order)
+courierAvailable(c: Courier)
+orderStuck(order: Order)
}
class Kitchen {
-center: DispatchMediator
+finishOrder(o: Order)
}
class Courier {
-center: DispatchMediator
+deliveryCompleted()
+assign(o: Order)
}
DispatchMediator <|.. DispatchCenter
Kitchen --> DispatchMediator : notifies
Courier --> DispatchMediator : notifies
DispatchCenter --> Courier : coordinates
DispatchCenter ..> Kitchen : coordinates
| GoF role | In PideYa |
|---|---|
| Mediator (coordination interface) | DispatchMediator |
| ConcreteMediator (the entire protocol) | DispatchCenter |
| Colleague (participants who know only the mediator) | Kitchen, Courier (and tomorrow CongestionMonitor, ExternalCourier...) |
The golden rule that defines the pattern: colleagues never reference each other. A colleague does its local work and, when something relevant to others happens, tells the mediator; the mediator decides whose turn it is to react and gives the order.
Complete Java implementation: DispatchCenter
The mediator interface. Its methods are the events the colleagues report — name them after what happened, not after what should be done (the mediator decides that):
public interface DispatchMediator {
void orderReady(Order order);
void courierAvailable(Courier courier);
void orderStuck(Order order);
}The colleagues, now lightweight: they do their thing and report. Courier no longer knows Kitchen; Kitchen no longer walks the couriers:
public class Kitchen {
private final DispatchMediator center;
public Kitchen(DispatchMediator center) { // injected, as always (DIP)
this.center = center;
}
public void finishOrder(Order order) {
order.markReadyForPickup(); // its own work
center.orderReady(order); // tell the mediator; done
}
}
public class Courier {
private final DispatchMediator center;
private final String zone;
private boolean available = true;
public Courier(DispatchMediator center, String zone) {
this.center = center;
this.zone = zone;
}
public void deliveryCompleted() {
this.available = true; // its own work
center.courierAvailable(this); // report; no idea what happens next
}
/** Order it receives FROM the mediator. */
public void assign(Order order) {
this.available = false;
this.currentOrder = order;
// pick up, navigate, etc.
}
public boolean isAvailable() { return available; }
public boolean acceptsZone(String z) { return zone.equals(z); }
}The concrete mediator: here, and only here, lives the complete coordination protocol — readable top to bottom in one class:
public class DispatchCenter implements DispatchMediator {
private final Queue<Order> waiting = new ArrayDeque<>();
private final List<Courier> fleet = new ArrayList<>();
public void registerCourier(Courier c) {
fleet.add(c);
}
@Override
public void orderReady(Order order) {
Optional<Courier> candidate = findAvailable(order.getZone());
if (candidate.isPresent()) {
candidate.get().assign(order);
} else {
waiting.add(order); // the protocol decides what to do
EventLog.INSTANCE.info("Order " + order.getId() + " waiting");
}
}
@Override
public void courierAvailable(Courier courier) {
waiting.stream()
.filter(o -> courier.acceptsZone(o.getZone()))
.findFirst()
.ifPresent(o -> {
waiting.remove(o);
courier.assign(o);
});
}
@Override
public void orderStuck(Order order) {
// reassignment: back to the queue with priority, alert support...
waiting.add(order);
}
private Optional<Courier> findAvailable(String zone) {
return fleet.stream()
.filter(Courier::isAvailable)
.filter(c -> c.acceptsZone(zone))
.findFirst();
}
}Look at what was gained:
- The protocol is a single text: "if there's a courier in the zone, assign; if not, queue; when someone frees up, check the queue". Before, it was scattered across three classes.
- Changing the assignment criterion ("the nearest" instead of "the first available") touches one line of one class. In fact, that criterion is a family of interchangeable algorithms... exactly what Strategy will reify in its lesson — the mediator will be its perfect client.
- Testing
Courieris now trivial: inject a fakeDispatchMediatorand done. - Adding a new colleague (the
CongestionMonitorthat detects long waits and callsorderStuck) touches neitherKitchennorCourier.
The conversation over time
The same scenario from the beginning — a courier frees up and an order is waiting — now as a star:
sequenceDiagram
participant K as Kitchen
participant C as DispatchCenter
participant R as Courier (María)
K->>C: orderReady(order 88)
C->>C: anyone available in zone? no → queue it
Note over R: María finishes another delivery
R->>C: courierAvailable(María)
C->>C: anything waiting for her zone? yes: order 88
C->>R: assign(order 88)
Note over K,R: Kitchen and María never spoke
Variants of the pattern
- With or without a Mediator interface: if there will only ever be one center, GoF allows skipping the interface and using the concrete class. We keep it for testability (fake mediators in the colleagues' tests) and because an
ExternalDispatchCenter(subcontractor) is plausible. - How does the colleague notify the mediator? Our version uses specific methods (
orderReady(...)), clear and typed. The alternative is a single generic method (notify(colleague, "ORDER_READY", data)) — more flexible, less safe; the moment you see it, you're one step from an event bus (a mention at Observer's doorstep and a full treatment in module 6). - UI mediator: GoF's canonical example is a dialog whose widgets (button, list, text field) coordinate through the dialog — "checking the box enables the button". If you've ever written a screen controller that coordinates its components, you've written a mediator.
The god-mediator risk
The classic criticism of the pattern, and it's fair: the mediator concentrates the coupling instead of eliminating it. That's good (it's localized, readable, changeable) until it isn't: the center that also computes routes, applies bonuses, decides shifts, and sends notifications becomes a god object — the system's biggest blob of responsibility, untouchable out of fear and a bottleneck for every change. Warning signs and antidotes:
- The mediator does domain work instead of coordinating → extract that work to the colleagues or to services (SRP: the mediator coordinates, the colleagues work).
- The mediator grows methods that only interest one pair of colleagues → maybe that pair deserves to talk directly, or to get its own small mediator.
- The algorithms inside the mediator vary → extract them as strategies (Strategy) and leave the mediator the orchestration.
The honest version of the contract: Mediator doesn't reduce the protocol's complexity — it moves it to a single place. If the protocol is intrinsically huge, the mediator will be big; the pattern owes you order, not miracles.
When to use it and when not to
Use it when:
- A set of objects collaborates in complex, changing ways, and the cross-references keep multiplying.
- You want to reuse or test the colleagues on their own, without dragging their interlocutors along.
- The coordination protocol deserves to be a named piece of its own, readable and modifiable in one go.
Avoid it when:
- There are only two collaborators with a simple relationship: a direct call (or an Observer) suffices; a mediator between two is bureaucracy.
- The "protocol" is trivial and stable: a two-thread mesh doesn't hurt.
- You'd be building the god mediator from day one: if the class is born with twenty responsibilities, the problem is domain partitioning, not topology.
Relationship with other patterns
- Observer: the other way to decouple communication — and the module's most frequent confusion. A one-line preview (full head-to-head in the comparison): Observer broadcasts "X happened" to whoever wants to hear it, with no central protocol; Mediator directs a protocol with known roles. Also, a mediator can be implemented by listening to its colleagues as an observer.
- Facade: superficially similar (one object in front of several). The facade offers a simplified one-way interface to a subsystem that doesn't know it; the mediator holds a two-way dialogue with colleagues who do know it.
- Strategy: the mediator's variable criteria (courier assignment) get extracted as strategies.
- Singleton: the temptation to make
DispatchCentera global singleton; better to inject it, for the reasons we already suffered in module 2.
Common mistakes
- Colleagues keeping references to each other "just for this case": the first cross-reference reopens the mesh; by the third, you have a mesh and a mediator (the worst of both worlds).
- The mediator that works: if
DispatchCentercomputes the route or charges for the delivery, it has stopped coordinating. Coordinating is deciding who and when; the how belongs to the colleagues. - Notification cycles: the mediator orders a colleague, the colleague notifies the mediator, which orders again... Protect yourself by distinguishing "event report" (colleague → mediator) from "order" (mediator → colleague) and don't report events from inside received orders.
- A mediator interface that mirrors one implementation: methods like
assignToFirstAvailable(...)bake the protocol into the interface; name them after events (orderReady) so you can change the policy without touching the colleagues. - Turning it into a junk drawer: "since everything passes through here, let's also put in the logging, the cache, and the metrics". Every new tenant brings the god mediator closer.
Exercises
Exercise 1: a new colleague without touching the old ones
Implement CongestionMonitor: every minute it reviews the waiting orders and, if any has waited more than 10 minutes, calls center.orderStuck(order). The center must then also notify support using the Notifier from module 2. State which classes get created, which get modified, and which remain untouched.
Exercise 2: mesh → star
Draw (or describe) the dependencies of these four classes before and after applying Mediator: Kitchen, Courier, TrackingScreen (shows the customer where their order is), and DispatchCenter. How many class-to-class references are there in each version?
Exercise 3: spot the god
A year later, DispatchCenter has 1,800 lines: it assigns couriers, computes routes with traffic, applies the rain bonus, manages shifts, and sends the push notifications. Propose a split: what stays in the mediator and where does everything else go? (Name previously seen patterns where they apply.)
Solutions
Solution 1: you create CongestionMonitor (a new colleague that knows only DispatchMediator); you modify only DispatchCenter (the body of orderStuck adds supportNotifier.send(...), with the notifier injected in its constructor); Kitchen, Courier, and the DispatchMediator interface remain untouched (the method already existed). That is the star's dividend: growing without touching the neighbors.
Solution 2: before (mesh): Kitchen→Courier, Courier→Kitchen, TrackingScreen→Kitchen, TrackingScreen→Courier — 4 references (and growing quadratically with every colleague). After (star): Kitchen→Mediator, Courier→Mediator, TrackingScreen→Mediator, and the mediator knows its colleagues — the complete coordination sits in 1 class and each colleague has exactly 1 dependency, testable with a fake mediator.
Solution 3: only the orchestration stays in DispatchCenter: receiving events, consulting the pieces, and ordering assignments. Out go: route computation to a RoutingService (a colleague/domain service); the assignment criterion and the rain bonus to interchangeable strategies (Strategy) that the center uses; shift management to its own ShiftManager (the center asks it for availability); the push notifications to the existing Notifier classes, invoked by the center but implemented outside — or better yet, triggered by the order's status changes via Observer, which is precisely the lesson after next.
Conclusion
Mediator reorders the collaboration's topology: from the mesh where kitchen, orders, and couriers all knew each other, to the star where each colleague knows only DispatchCenter and the coordination protocol lives whole, readable, and changeable, in a single place. You've seen the coupling arithmetic (n versus n²), the conversation in sequence, its variants, and the warning that always accompanies it: the mediator concentrates coupling — watch that it doesn't bloat into a god mediator, extracting work to colleagues and algorithms to strategies.
The next pattern changes the scene but not the family: from delivery to the customer's cart. Editing the cart means experimenting — add this, remove that, apply a coupon — and the customer wants to be able to go back without the cart exposing its guts for someone to photograph. Saving and restoring state without breaking encapsulation: that delicate balance is exactly Memento's specialty.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
