The Mediator pattern's own well-known liability is that the mediator can become a god object. How does that happen, and what concrete techniques keep it from happening?
answer
- One place → eventually every rule
- Symptoms: fan-out, churn hotspot, boolean soup
- Route, don't decide
- Split by conversation, not by screen
- Switch → handler-per-message dispatch table
basics
~20 sEvery new interaction rule gets added to the one mediator, so it grows a branch per case until it knows everything and everyone edits it. Fixes: split by cohesive interaction area, keep it routing-only with logic in colleagues, and use typed handlers instead of one giant switch.
solid answer
~50 sMediator centralizes interaction logic by design, so it has a built-in growth vector: each new colleague or rule appends another branch to `notify(sender, event)`. Left unchecked the class accumulates all business logic, references every colleague, changes for every requirement, and becomes a merge-conflict hotspot and single test bottleneck — a god object that violates Single Responsibility as badly as the mesh it replaced. Counter-measures: (1) split by cohesion — several small mediators per interaction area rather than one per screen or module, composed hierarchically if needed; (2) keep the mediator *thin* — it routes and sequences, while domain decisions stay in the colleagues or in domain services it calls; (3) replace the giant `switch(event)` with one handler object or method per message type, so the mediator becomes a dispatch table; (4) define narrow colleague-facing interfaces so it depends on roles, not concrete widgets; (5) watch metrics — class length, fan-out, cyclomatic complexity, churn — and treat a rising trend as the signal to split.
code
pseudocode · 15 lines// Growth vector: every rule appends a branch here
class OrderMediator {
notify(sender, event) {
if (event == "itemAdded") { ... }
else if (event == "couponApplied") { ...compute discount... } // deciding, not routing
else if (event == "addressChanged") { ... }
// ...and 30 more
}
}
// Thin dispatcher: one handler per message, mediator only routes
class OrderMediator {
handlers: Map<MessageType, Handler>
notify(msg) { handlers[msg.type].handle(msg, colleagues) }
}go deeper
Say all the rules pile into one class until it knows too much, and that splitting it into smaller mediators helps.
Name the symptoms (SRP violation, high fan-out, churn hotspot) and the basic fixes: several focused mediators, keep logic in colleagues.
Give concrete mechanics — handler-per-message dispatch, narrow role interfaces, extract computation into services, explicit state machine instead of flags — plus the metrics that trigger a split.
Frame it as centralization risk generally: the same failure appears as the god service, the smart ESB, the orchestrator that owns all business rules. Set organizational guardrails (ownership, budgets, review rules) and know when to abandon Mediator for events or an explicit workflow engine.
## Why the liability is intrinsic, not accidental Mediator's value proposition is "put the interaction rules in one place". Its liability is the same sentence read pessimistically: **one place** eventually means *every* rule. The pattern gives every future change a default destination, and defaults win. Typical progression: 1. v1: `notify(sender, event)` with three `if` branches. Clear and readable. 2. v2: a colleague is added; two more branches, plus a flag to remember state between notifications. 3. v3: someone needs validation results, so the mediator starts *computing* rather than routing. 4. v6: 1,200 lines, references to 14 colleagues, a persistence call, a feature flag, and three booleans nobody dares delete. Symptoms to name in an interview: - **Low cohesion**: the class has several unrelated reasons to change (SRP violation). - **High fan-out / efferent coupling**: it depends on nearly everything, so it cannot be unit-tested without a large fixture. - **Churn hotspot**: git history shows every feature touching it → merge conflicts and regression risk. - **Shotgun comprehension**: understanding *any* flow requires reading a method that handles all flows. - **Hidden state machine**: booleans/flags accumulated to remember what happened between notifications, with no explicit states or transitions. ## Techniques that actually work ### 1. Split by interaction cohesion, not by screen One mediator per *cohesive conversation* — checkout-payment coordination, shipping-address coordination — instead of one per page. Groups of rules that always change together belong together; groups that change independently belong apart. Mediators may compose: a parent mediator coordinates sub-mediators, each a colleague from the parent's point of view. ### 2. Keep the mediator thin — routing, not deciding The strongest discipline: the mediator answers *who reacts and in what order*; colleagues (or domain services) answer *what the reaction is*. If a mediator method computes a discount, that logic belongs in a pricing service the mediator calls. A thin mediator stays readable even with many colleagues. ### 3. Replace the switch with a dispatch table A single `notify` with a long `switch(event)` is where the growth is visible. Give each message a type and register one handler per type (`Map<MessageType, Handler>`), or use overloaded/`handle(SpecificEvent)` methods. Each rule becomes independently readable, testable, and reviewable, and adding a rule stops editing a shared method. This is the structure popularized by "mediator" libraries in the CQRS/request-handler style, where the mediator is only a dispatcher and all logic lives in handlers. ### 4. Depend on narrow role interfaces Have the mediator hold `Validatable`, `Enableable`, `Reloadable` rather than concrete widget classes. This limits what it *can* do, keeps it testable with stubs, and makes accidental scope creep visible in review. ### 5. Make implicit state explicit When the mediator sprouts flags to remember prior notifications, it is really a state machine. Extract an explicit State pattern or state enum with defined transitions rather than growing boolean soup. ### 6. Instrument and set a budget Track lines-of-code, number of injected collaborators, cyclomatic complexity, and commit churn on the mediator class. Agree a threshold ("more than ~8 collaborators or 300 lines → split") and enforce it in review or via a static-analysis rule. The trend matters more than the absolute number. ## Knowing when *not* to use Mediator at all Sometimes the god object is a signal that the pattern was the wrong tool: - Only two objects interact → let them talk directly. - The reactions are independent fan-out → plain Observer/events are lighter. - The "colleagues" are really one concept split badly → the fix is to merge them, not to coordinate them. - The coordination is a long-running business process → model it as an explicit workflow/saga/state machine, which is a mediator with first-class states, persistence, and compensation. ## Honest framing for the interview Mediator does not remove complexity; it *relocates* it and makes it visible. Visible complexity in one named class is easier to manage than invisible complexity spread over ten — but only if you keep splitting it as it grows. Saying that trade-off out loud, rather than presenting Mediator as a pure win, is what distinguishes a senior answer.
- You inherit a 2,000-line mediator. What is your first refactoring move?Do not split by size — split by cohesion, and get a safety net first. Characterize the existing behaviour with tests driven through the mediator's public notify surface, then group the branches by which colleagues each touches; clusters that share colleagues and change together become candidate sub-mediators. Extract the largest independent cluster behind a handler interface, keep the old entry point delegating so callers do not change, and repeat. Move any computation you find into domain services as you go.
- How do you tell a legitimately large mediator from a god object?Count reasons to change, not lines. A large but thin mediator with 40 short routing rules over one cohesive conversation is fine — every rule is the same kind of thing. A god object has *heterogeneous* responsibilities: routing plus validation plus persistence plus formatting, unrelated features arriving in the same class, and a git history where every team touches it. Fan-out to concrete types and cyclomatic complexity per method are better signals than total length.
A team lead who starts as a coordinator and ends up making every decision: nobody can act without them, they are in every meeting, and the team stalls when they are on holiday. The fix is not a better lead — it is delegating decisions back and splitting the coordination area.