Design Patterns (GoF and modern) Module 5 – Behavioral patterns: state and communication
Mediator: centralise many-to-many talk
Mediator puts the rules between objects in one coordinator: a checkout form and a chat room in code, and how it differs from Observer, Facade and an event bus.
What you will learn
- Replace a web of object-to-object calls with a mediator that coordinates colleagues
- Implement a form mediator and a chat-room mediator
- Split a growing mediator by use case without changing its behaviour
- Compare Mediator with Observer, Facade and an event bus
Before you start
What you’ll learn · Before you start
What you will learn
- Replace a web of object-to-object calls with a mediator that coordinates colleagues
- Implement a form mediator and a chat-room mediator
- Split a growing mediator by use case without changing its behaviour
- Compare Mediator with Observer, Facade and an event bus
Before you start
On this page
Mediator is a behavioural design pattern that moves the rules about how a group of objects interact into one object, the mediator. The objects it coordinates, its colleagues, stop referring to each other. Each one tells the mediator when something happens to it, and the mediator decides what the others should do, so a web of many-to-many calls becomes a star of one-to-many ones. In the Gang of Four catalogue the mediator encapsulates how a set of objects interact: they stay loosely coupled, and the interaction can change without changing them. Forms and dialog boxes, chat rooms, booking desks and workflow coordinators are its classic uses.
The problem: a web of references
Picture a checkout form with four widgets: an address field, a payment choice, a coupon field and a Pay button. Two rules connect them. Choosing cash on delivery disables the coupon field, and Pay works only when the whole form is valid. Written directly, the payment widget disables the coupon field itself, each field enables or disables the button, and the button asks every field whether it is valid.
From a web of references to a star
Text description of the diagram
The diagram has two parts, one above the other.
- "Without a mediator": four widgets, Address, Payment, Coupon and Pay button, are drawn in a square. Every pair is joined by a line with arrows at both ends, six lines in all, because each widget refers to every other widget and calls it. That is 12 one-way references between 4 widgets.
- "With a mediator": the same four widgets are each joined by one two-way line to a single box, CheckoutForm, the mediator, which holds every rule between the widgets. No line joins two widgets.
With n widgets the first design can need n × (n − 1) references, which is 90 for 10 widgets, while the second needs 2n, which is 20.
Every widget now knows the others. Four widgets can need 12 one-way references, and ten widgets 90. No widget can be reused on a form that lacks the others, one rule is spread across several classes, and adding a widget means editing several existing ones. The star needs one reference from each widget to the form and one back: 8 for four widgets, 20 for ten.
The roles
- The mediator, here
CheckoutForm, creates or receives its colleagues, hears about every change and applies each rule that involves more than one of them. - The colleagues, here the widgets, know only their mediator. A widget tells the form “I changed”; it never calls another widget.
- The messages run both ways: colleagues report to the mediator, and the mediator tells colleagues what to do, such as “disable yourself” or “clear your text”.
A checkout form in Python
"""Mediator: a checkout form whose widgets never talk to each other. Each widget tells the form when the user changes
it, and the form, the mediator, applies every rule that involves more than one widget."""
class Widget:
"""A colleague: it knows only its mediator."""
def __init__(self, form):
self.form = form
self.enabled = True
def changed(self):
self.form.widget_changed(self)
class AddressField(Widget):
def __init__(self, form):
super().__init__(form)
self.text = ""
def type(self, text):
self.text = text
self.changed()
def valid(self):
return len(self.text.strip()) >= 5
class PaymentChoice(Widget):
def __init__(self, form):
super().__init__(form)
self.method = None # "card" or "cash on delivery"
def choose(self, method):
self.method = method
self.changed()
class CouponField(Widget):
CODES = {"SAVE10"}
def __init__(self, form):
super().__init__(form)
self.code = ""
def enter(self, code):
if self.enabled: # a disabled field ignores typing
self.code = code
self.changed()
def valid(self):
return self.code == "" or self.code in self.CODES
class PayButton(Widget):
def __init__(self, form):
super().__init__(form)
self.enabled = False
class CheckoutForm:
"""The mediator: it creates the widgets, hears about every change and applies the rules between them."""
def __init__(self):
self.address = AddressField(self)
self.payment = PaymentChoice(self)
self.coupon = CouponField(self)
self.pay = PayButton(self)
def widget_changed(self, widget):
if widget is self.payment:
cash = self.payment.method == "cash on delivery"
self.coupon.enabled = not cash # rule 1: no coupons with cash on delivery
if cash:
self.coupon.code = ""
# rule 2: Pay only when the whole form is valid
self.pay.enabled = self.address.valid() and self.payment.method is not None and self.coupon.valid()
def show(self, step):
coupon = (self.coupon.code or "empty") if self.coupon.enabled else "disabled"
print(f"{step:30} coupon {coupon:9} pay {'ON' if self.pay.enabled else 'off'}")
form = CheckoutForm()
form.show("(the form opens)")
form.address.type("12 Lake Road")
form.show('type the address')
form.payment.choose("card")
form.show("choose card")
form.coupon.enter("SAVE1O")
form.show('type coupon "SAVE1O" (a typo)')
form.coupon.enter("SAVE10")
form.show('fix it to "SAVE10"')
form.payment.choose("cash on delivery")
form.show("choose cash on delivery")
form.coupon.enter("SAVE10")
form.show("try the coupon again")
form.address.type("")
form.show("clear the address")
widgets = [form.address, form.payment, form.coupon, form.pay]
links = [(type(w).__name__, name) for w in widgets for name, value in vars(w).items() if isinstance(value, Widget)]
print("references from one widget to another:", links or "none") Output
(the form opens) coupon empty pay off type the address coupon empty pay off choose card coupon empty pay ON type coupon "SAVE1O" (a typo) coupon SAVE1O pay off fix it to "SAVE10" coupon SAVE10 pay ON choose cash on delivery coupon disabled pay ON try the coupon again coupon disabled pay ON clear the address coupon disabled pay off references from one widget to another: none
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checkout.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Both rules live in one method, widget_changed(). The widget classes stay tiny: a coupon field knows how to hold a
code and whether the code is valid, and nothing about payment methods. The script plays a user. Choosing a card
enables Pay, a mistyped coupon disables it until the code is fixed, and choosing cash on delivery disables the coupon
field and clears the code while Pay stays enabled. Trying the coupon again changes nothing, because a disabled field
ignores typing, and clearing the address turns Pay off. The last line checks the promise of the pattern: no widget holds a
reference to another widget, only to the form.
Mediator, Observer, Facade and an event bus
Four patterns get confused with each other here, because all four put something between objects.
- An Observer subject announces its own changes to subscribers it does not know. Information flows one way.
- A mediator knows its colleagues and coordinates them in both directions.
- A Facade gives callers outside a subsystem one simpler interface. The classes behind it never call it.
- An event bus delivers named events from any publisher to any subscriber, and it knows no rules.
| Criterion | Observer | Mediator | Facade | Event bus |
|---|---|---|---|---|
| Who talks to whom | One subject to many observers | Colleagues to the mediator and back | Outside callers to the facade | Any publisher to any subscriber |
| Who holds the rules | Each observer, for itself | The mediator | The subsystem behind it | Nobody: subscribers decide alone |
| Does the middle know the others | Only as a list of callbacks | Yes, each colleague | Yes, the subsystem classes | No, only event names |
| Typical use | Displays of one changing value | Forms, chat rooms, booking desks | A simple entrance to a library | Loosely related modules of an application |
The patterns also combine. The widgets above report to their mediator through a callback, a small Observer link, and Martin Fowler describes an Event Aggregator as “a single source of events for many objects”: an event bus is a mediator that has given its rules away to its subscribers.
Exercise: a booking desk
Exercise · Medium · Python
A booking desk that coordinates a room, calendars and a notifier
booking.py has three colleagues that know nothing about each other: a Room (its capacity and the times it is reserved), a Calendar (each person's meetings) and a Notifier (the messages it has sent). Times are whole hours, and a meeting from start to end overlaps another when start < other_end and other_start < end.
Finish the mediator, BookingDesk:
book(title, start, end, attendees)books the meeting only if the room can seat everyone, the room is available for the whole time and every attendee is available. Then it reserves the room, adds the meeting to every attendee's calendar and sends each attendee"Invited: <title> <start>-<end>". It returnsTruewhen it booked the meeting, andFalsewhen it refused it (and then changes nothing).cancel(title)releases the room, removes the meeting from the calendars and sends each attendee"Cancelled: <title>". It returnsFalsefor a title it does not know.
The colleagues must keep knowing nothing about each other or the desk: every rule that involves two of them belongs in BookingDesk.
Starter code · booking.py
"""Three colleagues that know nothing about each other, and the mediator that coordinates them."""
def overlaps(a_start, a_end, b_start, b_end):
return a_start < b_end and b_start < a_end
class Room:
def __init__(self, name, capacity):
self.name, self.capacity = name, capacity
self.reservations = {} # title -> (start, end)
def is_free(self, start, end):
return not any(overlaps(start, end, s, e) for s, e in self.reservations.values())
def reserve(self, title, start, end):
self.reservations[title] = (start, end)
def release(self, title):
self.reservations.pop(title, None)
class Calendar:
def __init__(self):
self.meetings = {} # person -> {title: (start, end)}
def is_free(self, person, start, end):
return not any(overlaps(start, end, s, e) for s, e in self.meetings.get(person, {}).values())
def add(self, person, title, start, end):
self.meetings.setdefault(person, {})[title] = (start, end)
def remove(self, person, title):
self.meetings.get(person, {}).pop(title, None)
class Notifier:
def __init__(self):
self.sent = [] # (person, text)
def send(self, person, text):
self.sent.append((person, text))
class BookingDesk:
"""The mediator: every rule that involves more than one colleague lives here."""
def __init__(self, room, calendar, notifier):
self.room, self.calendar, self.notifier = room, calendar, notifier
self.attendees = {} # title -> attendees
def book(self, title, start, end, attendees):
# Replace these lines: check capacity, the room and every attendee first; then reserve, add and invite.
self.room.reserve(title, start, end)
return True
def cancel(self, title):
# Replace this line: release the room, remove the meeting from the calendars and tell the attendees.
return False The sample tests · test_booking.py
from booking import BookingDesk, Calendar, Notifier, Room
def desk(capacity=4):
return BookingDesk(Room("Banyan", capacity), Calendar(), Notifier())
def test_three_meetings_are_booked_and_invitations_sent():
"""books three meetings that do not overlap and invites every attendee"""
d = desk()
assert d.book("Stand-up", 9, 10, ["Asha", "Ravi"])
assert d.book("Design review", 10, 12, ["Asha", "Kiran"])
assert d.book("Retro", 15, 16, ["Ravi"])
assert d.room.reservations == {"Stand-up": (9, 10), "Design review": (10, 12), "Retro": (15, 16)}
assert ("Kiran", "Invited: Design review 10-12") in d.notifier.sent
assert len(d.notifier.sent) == 5
def test_a_meeting_in_a_taken_room_is_refused():
"""refuses a meeting that overlaps a booking of the room, and changes nothing"""
d = desk()
assert d.book("Stand-up", 9, 10, ["Asha"])
assert not d.book("Planning", 9, 11, ["Ravi"])
assert "Planning" not in d.room.reservations
assert d.calendar.is_free("Ravi", 9, 11)
assert d.notifier.sent == [("Asha", "Invited: Stand-up 9-10")]
def test_a_busy_attendee_blocks_the_meeting():
"""refuses a meeting when one attendee is already in another meeting at that time"""
d = desk()
d.calendar.add("Kiran", "Dentist", 14, 15)
assert not d.book("One-to-one", 14, 15, ["Asha", "Kiran"])
assert d.room.is_free(14, 15)
assert d.calendar.is_free("Asha", 14, 15)
def test_the_room_must_seat_everyone():
"""refuses a meeting with more attendees than the room has seats"""
d = desk(capacity=2)
assert not d.book("All hands", 11, 12, ["Asha", "Ravi", "Kiran"])
assert d.notifier.sent == []
def test_cancel_releases_the_room_and_tells_attendees():
"""cancels a meeting: the room and the calendars are free again and every attendee is told"""
d = desk()
d.book("Design review", 10, 12, ["Asha", "Kiran"])
assert d.cancel("Design review")
assert d.room.is_free(10, 12)
assert d.calendar.is_free("Asha", 10, 12) and d.calendar.is_free("Kiran", 10, 12)
assert d.notifier.sent[-2:] == [("Asha", "Cancelled: Design review"), ("Kiran", "Cancelled: Design review")]
assert not d.cancel("Design review")
def test_colleagues_do_not_refer_to_each_other():
"""keeps every rule in the desk: no colleague holds another colleague or the desk"""
d = desk()
d.book("Stand-up", 9, 10, ["Asha"])
for colleague in (d.room, d.calendar, d.notifier):
for value in vars(colleague).values():
assert not isinstance(value, (Room, Calendar, Notifier, BookingDesk)) A hint
Write book() as checks first, actions second. Return False as soon as one check fails, before anything is reserved, so a refused meeting leaves no trace. Keep a dictionary from each title to its attendees, so that cancel() knows whom to tell.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
A chat room with mute and private messages, in six languages Pro: opens with a Pro, Premium or Ultimate pass
A chat room is the textbook mediator: users send everything to the room, and the room decides who receives it. Here it gains rules for ignoring, muting and private messages, a check that no user ever holds another user, and the same room in five more languages.
In this part:
- The room’s rules in one place
- Checking that no user holds another user
- The same room in Java, TypeScript, Go, C++ and Rust
4 runnable examples (C++, Go, Java, Python, Rust, TypeScript, languages_session.py, session.py) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Keeping a mediator small, measured on a growing form Pro: opens with a Pro, Premium or Ultimate pass
A mediator collects rules, and on a busy form it can become the one class that every change touches. This part grows the checkout to seven widgets and five rules, splits its mediator by use case, and proves with 2,000 random user actions that the split changed nothing.
In this part:
- How a mediator turns into a god class, with numbers
- Splitting by use case, with a differential test
- What a colleague may know: the Law of Demeter
- Mediator or event bus, compared
1 runnable example (god_mediator.py, growth.py, split_mediators.py, widgets.py) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
How is the Mediator pattern different from the Observer pattern?
Observer is one-way broadcasting: a subject announces its own changes to observers it knows only as callbacks, and each observer decides for itself what to do. Mediator is two-way coordination: colleagues report their changes to the mediator, and the mediator, which knows every colleague, decides what the others must do, so the rules between the objects live in one place. In a checkout form, a price label observing a cart is Observer; a form object that disables the coupon field when cash on delivery is chosen is Mediator. The two often appear together, because colleagues can report to their mediator through an Observer-style callback.
-
Mid to senior
A checkout page's mediator has grown to 1,800 lines with about 140 branches, and every feature touches it. How do you split it without changing what the page does, and how do you prove that nothing changed?
-
Mid to senior
In a ticket-booking service, seats are held, then paid for, then the ticket is issued, and a failed payment must release the seats. One team wants a mediator for the flow, the other an event bus between the seat, payment and ticket modules. Which do you choose, and how do you defend it?
-
Senior to staff
A chat product's rooms are in-process mediators: each room object holds its members and delivers messages. The product now needs 50,000 connected users spread over 20 servers, with members of one room on different servers. What breaks, and what replaces the in-process mediator?
Key takeaways
- A mediator turns many-to-many references between colleagues into one-to-many: each colleague knows only the mediator, and the rules between colleagues live in one place.
- Colleagues report changes; the mediator applies the rules and tells the others what to do, so each colleague stays small and reusable.
- Observer announces one subject’s changes, a mediator coordinates peers in both directions, a facade simplifies a subsystem for outsiders, and an event bus routes events without rules.
References
- Design Patterns: Elements of Reusable Object-Oriented Software (Mediator) (Addison-Wesley (Pearson))
- Event Aggregator (Martin Fowler)
- Law of Demeter: general formulation (Northeastern University (the Demeter project))
- Object-oriented programming: an objective sense of style (ACM SIGPLAN Notices (Lieberherr, Holland and Riel))
- Built-in functions: vars() (Python 3.14) (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress