Mediator
Part of the free Low-Level Design course on LogicWiz, in the chapter: Behavioural patterns III. The class-diagram exercises run in the app.
Mediator: one desk that everyone talks to
"Rider imports the screen, the screen imports the order, and the order imports rider. Where does a change stop?"
Priya, drawing arrows on a whiteboard
Everyone knows everyone
SwiftKart's dispatch screen is the page an operator uses to get packed orders to riders. Three kinds of objects take part: Order, Rider and DispatchScreen. The rules are short:
- When an order is packed, give it to the first free rider.
- If no rider is free, the order waits in a queue.
- When a rider finishes a delivery, that rider takes the first waiting order.
- An operator can click to give any order to any rider by hand.
Arjun let each object react to its own events and call the others it needs:
public class Order {
private final List<Rider> riders;
private final Queue<Order> waiting;
private final DispatchScreen screen;
// id, status and the constructor are left out
public void markPacked() {
status = "PACKED";
screen.show("Order " + id + " is packed");
for (Rider r : riders) {
if (r.isFree()) {
r.take(this);
assignTo(r);
screen.show("Order " + id + " goes to rider " + r.id());
return;
}
}
waiting.add(this);
}
}
public class Rider {
private final Queue<Order> waiting;
private final DispatchScreen screen;
// id, free flag and the constructor are left out
public void finishDelivery() {
free = true;
screen.show("Rider " + id + " is free");
Order next = waiting.poll();
if (next != null) {
take(next);
next.assignTo(this);
screen.show("Order " + next.id() + " goes to rider " + id);
}
}
}
DispatchScreen holds a List<Rider> and a List<Order>, and its clickAssign method calls rider.take(order) and order.assignTo(rider) itself. So the rule "give this order to this rider" now exists in three classes. Three things hurt:
- A rule change touches every class. Product wants the nearest free rider, not the first. Arjun edits
Order,RiderandDispatchScreen. If he misses one, two riders can be given the same order. - A new participant touches every class. Product wants a push message to the customer when a rider is assigned. That needs a new call in all three assignment places.
- Nothing can be tested alone. To test
Rider, you need anOrder, a queue and a screen.
The links grow fast. If every object may call every other, n objects need up to n times n minus 1 references: 6 for three objects, 12 for four, 20 for five.
Arjun objected: "Each object should handle its own events. Isn't that object-oriented?" For its own state, yes. A rider knows whether it is free. But "who gets this order?" involves a rider, an order and the queue. It belongs to none of them.
The idea in plain words, and its structure
Think of an airport. Planes do not radio each other to decide who lands first. Each plane talks to the control tower. The tower knows every plane, applies the rules, such as one plane on the runway at a time, and tells each plane what to do. A new plane learns one thing: how to talk to the tower.
Where the analogy stops: a real tower is a team of trained people, and pilots can still talk to each other in an emergency. In code, the tower is one class, and it can grow into a class that does everything. A later section is about that risk.
This is the Mediator pattern. The mediator is an object that other objects, called colleagues, talk to instead of talking to each other. A colleague reports what happened. The mediator decides who must do what, and tells them.
Count the references again. With a mediator, each colleague holds one reference, to the mediator, and the mediator holds one reference per colleague. That makes 2n. For three objects it is 6, the same as before. For four it is 8 against 12, and for five it is 10 against 20. At three objects the count barely changes, and the real gain is different. The rule now lives in one place, and a new participant touches one class.
The structure. The pattern has three roles:
- Mediator: the interface colleagues call. Here it is
Dispatcher. - Concrete mediator: the class with the rules. Here it is
DispatchCenter. - Colleagues: the objects that report events, here
Order,RiderandDispatchScreen.
classDiagram
class Dispatcher {
<<interface>>
+orderPacked(order)
+riderFree(rider)
+assignManually(order, rider)
}
Dispatcher <|.. DispatchCenter
Order --> Dispatcher
Rider --> Dispatcher
DispatchScreen --> Dispatcher
DispatchCenter --> Rider
DispatchCenter --> DispatchScreen
Colleagues point at the interface only. The center points at the colleagues, because it must tell them what to do.
Build it step by step
Step 1: the mediator interface.
public interface Dispatcher {
void orderPacked(Order order);
void riderFree(Rider rider);
void assignManually(Order order, Rider rider);
}
Each method names an event that happened, such as orderPacked. It does not say what to do about it. Colleagues report. The mediator decides.
Step 2: colleagues keep their own state and report events.
public class Rider {
private final String id;
private final Dispatcher dispatcher;
private boolean free = true;
public Rider(String id, Dispatcher dispatcher) {
this.id = id;
this.dispatcher = dispatcher;
}
public String id() { return id; }
public boolean isFree() { return free; }
public void take(Order order) { free = false; }
public void finishDelivery() {
free = true;
dispatcher.riderFree(this);
}
}
Order.markPacked has the same shape: it sets the status to PACKED, then calls dispatcher.orderPacked(this). DispatchScreen.clickAssign is one line: dispatcher.assignManually(order, rider). No colleague holds another colleague.
Step 3: the rules move into the center.
public class DispatchCenter implements Dispatcher {
private final List<Rider> riders = new ArrayList<>();
private final Queue<Order> waiting = new ArrayDeque<>();
private DispatchScreen screen;
public void addRider(Rider rider) { riders.add(rider); }
public void setScreen(DispatchScreen screen) { this.screen = screen; }
@Override
public void orderPacked(Order order) {
Rider free = firstFreeRider();
if (free == null) {
waiting.add(order);
screen.show("Order " + order.id() + " waits, no free rider");
} else {
assign(order, free);
}
}
@Override
public void riderFree(Rider rider) {
screen.show("Rider " + rider.id() + " is free");
Order next = waiting.poll();
if (next != null) {
assign(next, rider);
}
}
// assignManually, assign and firstFreeRider follow
}
// more methods of DispatchCenter
@Override
public void assignManually(Order order, Rider rider) {
if (!rider.isFree()) {
screen.show("Rider " + rider.id() + " is busy, order " + order.id() + " stays queued");
return;
}
waiting.remove(order);
assign(order, rider);
}
private void assign(Order order, Rider rider) {
rider.take(order);
order.assignTo(rider);
screen.show("Order " + order.id() + " goes to rider " + rider.id());
}
firstFreeRider loops over the riders and returns the first free one. The rule "give this order to this rider" now exists once, in assign. Choosing the nearest rider changes firstFreeRider, in one file. Each colleague is easy to test: pass a fake Dispatcher that records the calls.
Step 4: connect and run. The mediator and its colleagues need each other, so create the mediator first. Colleagues receive it in their constructors, and the center is introduced to them with addRider and setScreen.
DispatchCenter center = new DispatchCenter();
DispatchScreen screen = new DispatchScreen(center);
center.setScreen(screen);
Rider r1 = new Rider("R1", center);
Rider r2 = new Rider("R2", center);
center.addRider(r1);
center.addRider(r2);
// orders 101 to 104 are created with new Order(101, center), and so on
Now pack orders 101, 102 and 103, let R1 finish, pack 104, and let the operator click R2 for order 104:
[screen] Order 101 goes to rider R1
[screen] Order 102 goes to rider R2
[screen] Order 103 waits, no free rider
[screen] Rider R1 is free
[screen] Order 103 goes to rider R1
[screen] Order 104 waits, no free rider
[screen] Rider R2 is busy, order 104 stays queued
Order 103 waits until R1 is free. The operator's click on R2 is refused by the one place that knows the rule.
Step 5: add the customer push. A CustomerApp class has a push(orderId, message) method. Only the center changes, and it changes in one place:
// in DispatchCenter, which gets a new field: private CustomerApp customerApp;
private void assign(Order order, Rider rider) {
rider.take(order);
order.assignTo(rider);
screen.show("Order " + order.id() + " goes to rider " + rider.id());
customerApp.push(order.id(), "Rider " + rider.id() + " is on the way");
}
Order, Rider and DispatchScreen are untouched. CustomerApp only receives, so it needs no reference to the dispatcher.
Mediator or Observer?
In a mock interview, a candidate said: "Mediator is Observer with extra steps." Both reduce direct links between objects, so they look alike. The difference is who knows whom, and who makes decisions.
| Observer | Mediator | |
|---|---|---|
| Who knows whom | The subject knows its listeners only through an interface. Listeners know nothing about each other. | Each colleague knows only the mediator. The mediator knows every colleague. |
| Who coordinates | Nobody. Each listener decides for itself what to do. | The mediator. It holds the rules and decides. |
| What flows | Announcements from one subject to many listeners. | Events in from many colleagues, instructions out to others. |
| A new participant | Subscribe. Nothing else changes. | Edit the mediator. |
The two can also be combined, because a mediator may listen to each colleague's events as an observer would. The intent still decides the name. If receivers act on their own, it is Observer. If one object tells the others what to do, it is Mediator.
A third look-alike is Facade. A facade simplifies calls going one way, from a client into a subsystem, and the subsystem does not know the facade exists. In a mediator, colleagues know the mediator, and calls go both ways.
Keeping the mediator thin, and when not to use it
Everything flows through the center, so everything wants to live there: delivery fees, rider ratings, message text, refunds. Soon DispatchCenter has 1,000 lines, and every team edits it. That is a god object, a class that knows and does too much. The pattern moved the mess instead of removing it.
⚠️ Warning: A mediator should coordinate, not compute. When it does arithmetic or builds message text, move that work into another class.
Four habits keep it thin:
- Hand out the choices. Put "which rider?" behind a strategy interface, such as
RiderChooser. The center calls it and does not own the rule. - Keep colleague rules with colleagues. A rider decides whether it is free. The center asks.
- One mediator per group. Riders and orders have one. Payments have another. Never one mediator for the whole app.
- Watch the signs. It imports almost every class, or unrelated features land in it.
Use a mediator when a small group of objects follow shared rules that involve several of them at once, and new participants or rule changes are likely. Do not use one when:
- two objects talk in one clear way, because a direct call is clearer;
- the messages are only announcements, which is Observer;
- the mediator would only forward calls and apply no rule, which adds a hop and no value.
It also has costs. To see who called whom, you read the center. Colleagues need a dispatcher, so reusing a Rider elsewhere means supplying one.
The chat room, and practice
The classic interview example is a chat room. Users must not hold each other's references. They talk to the room, and the room delivers the messages.
public class ChatRoom {
private final Map<String, User> members = new LinkedHashMap<>();
private final Set<String> muted = new HashSet<>();
public void join(User user) { members.put(user.name(), user); }
public void mute(String name) { muted.add(name); }
public void broadcast(String from, String text) {
if (muted.contains(from)) return;
for (User u : members.values()) {
if (!u.name().equals(from)) u.receive(from, text);
}
}
public void sendPrivate(String from, String to, String text) {
User target = members.get(to);
if (target != null && !muted.contains(from)) {
target.receive(from, text);
}
}
}
User holds only the room. Its say(text) calls room.broadcast(name, text), and whisper(to, text) calls room.sendPrivate. With three users, user1 says "hello all", user2 whispers to user3, and then user1 is muted and says something more:
user2 sees user1: hello all
user3 sees user1: hello all
user3 sees user2: see you at 5
The muted message reaches nobody, and the rule sits in one place. Every follow-up an interviewer asks, such as history, blocking or a member limit, changes only ChatRoom.
You will meet the same shape in air traffic control, smart-home hubs and dialog boxes where one checkbox enables a text field.
The starter has three classes holding each other. Introduce a mediator interface and a center that implements it, and cut every direct link between the three.
📌 Summary: A mediator is the one object colleagues talk to instead of each other, and it holds the coordination rules. Unlike Observer it decides. Keep it thin, and skip it for two objects.
What Arjun learns next
The dispatch code is tidy now. Then a support agent edits an order by mistake and wants the old version back. That needs a safe way to save an object's earlier state and restore it later.
Check your understanding
- With the DispatchCenter in place, Product asks for the nearest free rider instead of the first free one. What has to change?
- OrderService announces a status change. The SMS, email and push notifiers each decide for themselves whether to send. Which pattern is this?
- After a year, DispatchCenter has 1,200 lines. It also works out delivery fees and writes SMS text. What is the best fix?
- Cart calls CouponService once at checkout and nothing else talks to either class. A teammate proposes a CheckoutMediator. What do you say?