LLD Dojo

Mediator

Occasional. Turns up in specific problems. Worth recognising and being able to sketch, not worth drilling.

Start with the problem

Take the ride-share domain corpus/trip-state-machine already builds on: a Driver object and a Rider object, matched for one trip. The obvious first version has each one hold a reference to the other. When the driver's location updates, Driver calls rider.onDriverEtaUpdated(eta) directly. When the rider cancels, Rider calls driver.onRideCancelled() directly. Two classes, two references, nothing complicated.

Watch where it goes

A support team asks for every ETA update to land in an audit log, for tickets where a rider disputes how long they waited. Now Driver.updateEta has to call rider.onDriverEtaUpdated(eta) and write to the support log. A billing team then asks to hear about cancellations too, so a fare hold can be released. Rider.cancel now calls driver.onRideCancelled() and notifies billing.

Neither requirement changed what a driver or a rider fundamentally does. Each one added a new party that has to be told about an interaction between two other objects, and each new party meant opening Driver or Rider again to add one more direct call. Driver and Rider end up holding references to everything that might ever care about a trip, and testing either one means constructing that whole web first.

The move

Pull the routing out into one object that both Driver and Rider talk to, and that neither of them holds a reference to each other through.

interface RideMediator {
    void driverEtaUpdated(String rideId, Duration eta);
    void riderCancelled(String rideId);
}

final class DispatchMediator implements RideMediator {
    private final Map<String, Rider> ridersByRide;
    private final SupportLog supportLog;
    private final PricingHolds pricingHolds;

    DispatchMediator(Map<String, Rider> ridersByRide, SupportLog supportLog, PricingHolds pricingHolds) {
        this.ridersByRide = ridersByRide;
        this.supportLog = supportLog;
        this.pricingHolds = pricingHolds;
    }

    @Override
    public void driverEtaUpdated(String rideId, Duration eta) {
        ridersByRide.get(rideId).onDriverEtaUpdated(eta);
        supportLog.record(rideId, eta);
    }

    @Override
    public void riderCancelled(String rideId) {
        pricingHolds.release(rideId);
        // notify the matched driver through the same registry, if one is still assigned
    }
}

Driver now holds a RideMediator, not a Rider, and calls mediator.driverEtaUpdated(rideId, eta). Rider holds the same mediator and calls mediator.riderCancelled(rideId). Neither class depends on the other, or on the support log, or on billing. The mediator is the one place that knows who else needs to hear about a trip event, and it is the only class that changes when a fourth party joins.

This is Mediator: an object that colleague classes talk through instead of talking to each other directly. Adding a new listener to an interaction now costs one class, not an edit to every participant in it.

What modern Java changes here

Most Java systems get this shape from an event bus or a message broker rather than a hand-written RideMediator interface. Spring's ApplicationEventPublisher does the same job: accept a message from one participant and decide who else needs it, without the publisher naming its subscribers. A queue that Driver publishes to, with the support log and billing subscribing separately, works the same way. At that point the GoF name is a label for plumbing a framework already provides, not something to build by hand.

For a small, in-process case with only a few colleagues, a Map<String, List<Consumer<Event>>> registry is often enough to get the same routing without naming an interface at all.

When naming it is wrong

Two participants that will only ever be two do not need a mediator between them. The direct-call version from the start of this page is fine as long as a third party is hypothetical, not something anyone has actually asked for. Reach for this pattern once a third distinct kind of participant is named in the requirements, because that is the moment "who talks to whom" stops being answerable by reading two classes.

Where this lives in the app

No corpus problem in this app builds a mediator. The Driver, Rider and DispatchMediator classes above were built for this page, in the domain of corpus/trip-state-machine's ride-share trip, and are not code that exists in that corpus problem.

The anchor given is C1, the single-responsibility lesson built around the "write the sentence, then look for the word 'and'" test. Reading it confirms what it actually teaches: how to notice that one class is doing the job of several, using corpus/file-system's decomposition as the worked case. That is the discipline that would lead someone toward pulling coordination logic into its own class once a god class turns out to be coordinating other objects. C1 does not build a mediator. It is the test that catches the god class a mediator would go on to fix.

All reference pages