LLD - Mediator Design Pattern

Manish PushkarManish Pushkar
7 min read

Mediator Design Pattern

Mediator Pattern is a behavioural design pattern that defines an object called a Mediator

  • to centralize complex communications

  • control the interactions between multiple objects called colleagues

  • reduce direct dependencies between objects

Case Study - Smart Home Automation

Imagine you are designing Smart Home Automation where different smart devices like Lights, Thermostats, Sprinklers, Alarms, and others can interact with each other automatically.

For example:

  • When the Thermostat detects that the temperature has risen above a threshold, it might turn on the Sprinkler to water the garden.

  • When the Alarm goes off, it might turn on all Lights and disable the Sprinkler.

Naive Solution

  • Each device will directly talk to the other device it needs to coordinate with.

  • Thermostat will have reference of Sprinkler

  • Alarm will have references to Light and Sprinkler.

  • Light may or may not know about others.

public class Thermostat {
    private Sprinkler sprinkler;

    public Thermostat(Sprinkler sprinkler) {
        this.sprinkler = sprinkler;
    }

    public void temperatureChanged(int temperature) {
        if (temperature > 30) {
            sprinkler.turnOn();
        }
    }
}

public class Sprinkler {
    public void turnOn() {
        System.out.println("Sprinkler - Turned On");
    }

    public void turnOff() {
        System.out.println("Sprinkler - Turned Off");
    }
}

public class Light {
    public void turnOn() {
        System.out.println("Light - Turned ON");
    }
}

public class Alarm {
    private Light light;
    private Sprinkler sprinkler;

    public Alarm(Light light, Sprinkler sprinkler) {
        this.light = light;
        this.sprinkler = sprinkler;
    }

    public void alarmTriggered() {
        light.turnOn();
        System.out.println("Alarm: Turning off Sprinkler");
        sprinkler.turnOff();
    }
}

Issues with this design

  • Tight Coupling - Devices know about the internal state of other devices

  • Hard to add new devices - It can lead to modifying many existing classes

  • Violates Single Responsibility Principles - Especially Open-Closed Principles (classes should be open for extension and closed for modification)

Solution - With Mediator Pattern

public interface ISmartHomeMediator {
    void registerDevice(Device device);
    void notify(Device sender);
}
import java.util.ArrayList;
import java.util.List;

public class ConcreteSmartHomeMediator implements ISmartHomeMediator {
    private List<Device> devices = new ArrayList<>();

    @Override
    public void registerDevice(Device device) {
        devices.add(device);
    }

    @Override
    public void notify(Device sender) {
        if (sender instanceof Thermostat) {
            System.out.println("Mediator - Thermostat event received. Turn on Sprinkler");
            for (Device device : devices) {
                if (device instanceof Sprinkler) {
                    ((Sprinkler) device).waterGarden();
                }
            }
        } else if (sender instanceof Alarm) {
            System.out.println("Mediator - Alarm event received. Turn on Light. Turn off Sprinkler");
            for (Device device : devices) {
                if (device instanceof Light) {
                    ((Light) device).turnOn();
                } else if (device instanceof Sprinkler) {
                    ((Sprinkler) device).stopWatering();
                }
            }
        }
    }
}
public interface Device {
    void updateState();
}
public class Light implements Device {
    private final ISmartHomeMediator mediator;

    public Light(SmartHomeMediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void updateState() {
        System.out.println("Light - State Updated");
        mediator.notifyDevice(this);
    }

    public void turnOn() {
        System.out.println("Light - Turned On");
    }
}
public class Thermostat implements Device {
    private final ISmartHomeMediator mediator;

    public Thermostat(SmartHomeMediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void updateState() {
        System.out.println("Thermostat - State Updated");
        mediator.notifyDevice(this);
    }
}
public class Sprinkler implements Device {
    private final ISmartHomeMediator mediator;

    public Sprinkler(SmartHomeMediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void updateState() {
        System.out.println("Sprinkler - State Updated");
        mediator.notifyDevice(this);
    }

    public void turnOn() {
        System.out.println("Sprinkler - Turned On");
    }

    public void turnOff() {
        System.out.println("Sprinkler - Turned Off");
    }
}
public class Alarm implements Device {
    private final ISmartHomeMediator mediator;

    public Alarm(SmartHomeMediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void updateState() {
        System.out.println("Alarm - State Updated");
        mediator.notifyDevice(this);
    }
}
public class Main {
    public static void main(String[] args) {
        ISmartHomeMediator mediator =  new SmartHomeMediator();
        Light light = new Light(mediator);
        Thermostat thermostat = new Thermostat(mediator);
        Sprinkler sprinkler = new Sprinkler(mediator);
        Alarm alarm = new Alarm(mediator);

        mediator.registerDevice(light);
        mediator.registerDevice(thermostat);
        mediator.registerDevice(sprinkler);
        mediator.registerDevice(alarm);

        System.out.println("Thermostat - Temperature Changes");
        thermostat.updateState();

        System.out.println("Alarm - Triggered");
        alarm.updateState();
    }
}

Extension of Case Study - Introducing Two Mediators

Lets introduce more devices and group them based on the functional domain

  • Environment: Light, Thermostat, Sprinkler

  • Security: Alarm, DoorLock, Camera

Originally, you used one mediator - SmartHomeMediator to manage everything. But as the number of devices & rules grows, this mediator risks becoming God's object.

So the solution is to split into Two Mediators for separation of concerns & maintainability.

Solution -

Here, Mediator holds all device instances -
This is okay if the system is small and static. And the interactions are explicit and tightly controlled.

Alternatively, you can maintain a list of devices that can register it dynamically, as shown above.

Interfaces

public interface ISmartDevice {
    void notifyEvent(String event);
}

public interface IMediator {
    void notify(ISmartDevice sender, String event);
}

Mediator

public class EnvironmentMediator implements IMediator {
    private Light light;
    private Thermostat thermostat;
    private Sprinkler sprinkler;

    public void registerDevices(Light light, Thermostat thermostat, Sprinkler sprinkler) {
        this.light = light;
        this.thermostat = thermostat;
        this.sprinkler = sprinkler;
    }

    @Override
    public void notify(ISmartDevice sender, String event) {
        if (sender instanceof Sprinkler && "RAIN_DETECTED".equals(event)) {
            System.out.println("[EnvironmentMediator] Rain detected, turning off sprinklers.");
            sprinkler.turnOff();
        } else if (sender instanceof Thermostat && "TEMP_DROP".equals(event)) {
            System.out.println("[EnvironmentMediator] Temperature drop detected, turning on lights.");
            light.turnOn();
        }
    }
}
public class SecurityMediator implements IMediator {
    private Alarm alarm;

    public void registerDevices(Alarm alarm) {
        this.alarm = alarm;
    }

    @Override
    public void notify(ISmartDevice sender, String event) {
        if ("MOTION_DETECTED".equals(event)) {
            System.out.println("[SecurityMediator] Motion detected, triggering alarm.");
            alarm.trigger();
        }
    }
}

Case Study - Chat Room System (Slack Group Chat)

Design a group chat system where multiple users can communicate with each other. A message should be delivered to all other users in the same chat room when a user sends it.

public class User {
    private final String name;
    private final IChatRoom chatRoom;

    public User(String name, IChatRoom chatRoom) {
        this.name = name;
        this.chatRoom = chatRoom;
    }

    public String getName() {
        return name;
    }

    public void sendMsg(String msg) {
        System.out.println(name + " sends " + msg);
        chatRoom.sendMessage(msg, this);
    }

    public void receiveMsg(String msg, String senderName) {
        System.out.println(name + " received msg from " + senderName + " : " + msg);
    }

}
public interface IChatRoom {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

public class ChatRoom implements IChatRoom {
    List<User> users = new ArrayList<>();

    @Override
    public void sendMessage(String message, User sender) {
        for (User user : users) {
            if (!user.equals(sender)) {
                user.receiveMsg(message, sender.getName());
            }
        }
    }

    @Override
    public void addUser(User user) {
        users.add(user);
    }
}
public class ChatRoomMain {
    public static void main(String[] args) {
        IChatRoom chatRoom = new ChatRoom();

        User peter =  new User("Peter", chatRoom);
        User miles = new User("Miles", chatRoom);
        User vulture =  new User("Vulture", chatRoom);

        chatRoom.addUser(peter);
        chatRoom.addUser(miles);
        chatRoom.addUser(vulture);

        peter.sendMsg("hey I am back from multiverse");
        vulture.sendMsg("Lets meet Peter");
    }
}

Output:

Case Study - Auction System

Let’s consider a case where we have an online auction system where multiple bidders can place bids. Each bidder needs to know the current highest bid and participate in the auction without directly communicating with each other.

interface BidMediator {
    void placeBid(Bidder bidder, double amount);
    void addBidder(Bidder bidder);
}

class AuctionMediator implements BidMediator {
    private List<Bidder> bidders;
    private double highestBid = 0;

    public AuctionMediator() {
        this.bidders = new ArrayList<>();
    }

    @Override
    public void placeBid(Bidder bidder, double amount) {
        if (amount > highestBid) {
            highestBid = amount;
            System.out.println(bidder.getName() + " placed the highest bid: " + highestBid);
            notifyAllBidders();
        }
    }

    @Override
    public void addBidder(Bidder bidder) {
        bidders.add(bidder);
    }

    private void notifyAllBidders() {
        for (Bidder bidder : bidders) {
            bidder.update(highestBid);
        }
    }
}

abstract class Bidder {
    protected BidMediator mediator;
    protected String name;

    public Bidder(BidMediator mediator, String name) {
        this.mediator = mediator;
        this.name = name;
    }

    public abstract void placeBid(double amount);
    public abstract void update(double highestBid);
}

class ConcreteBidder extends Bidder {
    public ConcreteBidder(BidMediator mediator, String name) {
        super(mediator, name);
    }

    @Override
    public void placeBid(double amount) {
        System.out.println(name + " places a bid: " + amount);
        mediator.placeBid(this, amount);
    }

    @Override
    public void update(double highestBid) {
        System.out.println(name + " is notified of the new highest bid: " + highestBid);
    }
}

public class AuctionDemo {
    public static void main(String[] args) {
        BidMediator mediator = new AuctionMediator();
        Bidder bidder1 = new ConcreteBidder(mediator, "Alice");
        Bidder bidder2 = new ConcreteBidder(mediator, "Bob");

        mediator.addBidder(bidder1);
        mediator.addBidder(bidder2);

        bidder1.placeBid(100);
        bidder2.placeBid(150);
    }
}

When to use Mediator?

  • Complex many to many interactions between components

  • Communication rules change frequently

  • You want to keep components decoupled and reusable

Pros of Mediator Pattern

  • Reduces coupling between components/colleagues as they don’t talk to each other directly

  • Centralizes complex communication through mediator

  • Promotes Single Responsibility Principle - Components focus on their behavior and the mediator handles communication logic

Cons of Mediator Pattern

  • Mediator can become a God Object. If not designed carefully, mediator ends up handling too much logic

  • For performance critical applications, centralized message dispatching can become a bottleneck

Mediator Pattern vs Observer Pattern

MedaitorObserver
Centralizes complex communication logic between multiple components (many to many)Broadcasts state changes from one subject to many observers (one to many)
Components talk to a mediator which then talks to othersA subject notifies observers directly about the state changes
0
Subscribe to my newsletter

Read articles from Manish Pushkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Manish Pushkar
Manish Pushkar

Software Engineer - Backend