# Mediator

> Route interactions between a group of components through one coordinator so they stop referencing each other and the rules live in one place.

- kind: pattern
- category: behavioral
- aliases: Intermediary, Controller
- canonical: https://aipatterns.dev/design-patterns/mediator
- inspired-by: https://refactoring.guru/design-patterns/mediator

## Intent

Stop a set of components from calling each other directly. Each component reports what happened to one mediator object, and the mediator decides which other components to update. The coordination rules move out of the components and into a single place you can read and test.

## Forces

- **Components that must react to each other** vs **components you want to reuse alone**: a room picker that calls the price label can't be used on a page without one.
- **Rules spread across peers** vs **rules you can read in one place**: "more guests filters rooms, and any change reprices" ends up split over four event handlers.
- **N components** vs **N×N links**: every new component adds wiring to every existing one.
- **One central coordinator** vs **that coordinator growing without bound**: the cure for tangled peers can become a god object.

## Therefore

**Give the components a reference only to a mediator interface, have them report changes to it, and put every cross-component rule in the mediator.**

Components (often called colleagues) stay simple: they hold their own state, expose methods to update it, and call `mediator.changed(event)` when the user acts. Methods the mediator calls on a component do not notify back, which avoids feedback loops. The mediator is specific to one workflow. When it starts to serve several unrelated workflows, split it.

## Structure

```mermaid
classDiagram
  class PanelMediator {
    <<interface>>
    +changed(event) void
  }
  class BookingPanel {
    -Room[] catalog
    +changed(event) void
  }
  class NumberInput {
    -PanelMediator mediator
    -int value
    +set(value) void
  }
  class RoomSelect {
    -PanelMediator mediator
    -Room selected
    +choose(code) void
    +limitTo(rooms) void
  }
  class QuoteLabel {
    -String text
    +show(text) void
  }
  PanelMediator <|.. BookingPanel
  BookingPanel *-- NumberInput
  BookingPanel *-- RoomSelect
  BookingPanel *-- QuoteLabel
  NumberInput --> PanelMediator
  RoomSelect --> PanelMediator
```

### Participants

- **Mediator** (`PanelMediator`): the narrow interface components use to report events. Components never see more than this.
- **Concrete mediator** (`BookingPanel`): owns the components and holds every rule about how they affect each other.
- **Colleagues** (`NumberInput`, `RoomSelect`, `QuoteLabel`): hold their own state and notify the mediator. They do not reference each other.
- **Client**: creates the mediator and talks to the colleagues through it, or lets user input drive them.

## Use it when

- A group of UI widgets, services or devices has many-to-many update rules, and changing one rule means editing several components.
- You want to reuse a component in another screen or flow, but it references its current neighbors directly.
- The interaction rules are the hard part of the feature and need their own tests.
- Adding a component today means touching every existing component. See [Shotgun Surgery](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md).

## Do not use it when

- One component updates one other, in one direction. A direct call or a callback is clearer.
- Components only need to announce events and listeners don't coordinate with each other. Use [Observer](https://aipatterns.dev/design-patterns/observer.md) or an event emitter.
- The coordination is one-way data flow: one owner of state, widgets that only derive from it. Lift the state to a parent, or use a store (Redux, Pinia, a Svelte store) or a view model, and derive each widget's props from that state. A mediator earns its name only when peers talk back and the coordination has rules of its own.
- The mediator would coordinate unrelated workflows. Several small mediators beat one god object that every feature edits. If yours passes a few hundred lines or has a `switch` over dozens of events, split it by workflow.
- The "mediator" only forwards calls to a subsystem without coordinating peers. That is a [Facade](https://aipatterns.dev/design-patterns/facade.md).

## Application grill

**Inspect first:** the component tangle the pattern would untie (who calls whom for one workflow); existing event mechanisms (an event emitter, signals, streams, a message broker) - a mediator over an existing bus may be configuration, not code.

**Missing pieces:** in-process: the language's event emitter or a small coordinator object - nothing to buy. Cross-service: a broker (Redis pub/sub for transient fan-out, Kafka or SQS for durability and ordering), which brings retention and ordering semantics to decide - do not add one for a single coordinator.

**Ask before you build:**

- **Scale:** how many components does the mediator join - three is an object, fifteen is an architecture?
- **Performance:** is it on the request path (synchronous) or the event path (asynchronous, with latency tolerance)?
- **Reliability:** when one participant fails, who notices - what is the mediator's error policy?
- **Budget:** is a broker justified for one interaction, or is in-process enough until a second service needs the events?
- **Implementation:** are interactions typed (request and response through the mediator) or fire-and-forget?

## Implementation steps

1. List the components and draw every place one references another. Each arrow is a rule to move.
2. Define a mediator interface with one notification method, for example `changed(event)`. Keep it this narrow.
3. Replace each component's references to its peers with a reference to the mediator interface.
4. Move every cross-component rule into the concrete mediator. Methods the mediator calls on components must not notify back.
5. Have the mediator create or receive the components and set the initial state in one place.
6. Test the mediator's rules directly: change one component and assert the state of the others.

## Example

A hotel booking panel has a guest count, a night count, a room picker and a price quote. More guests than a room holds removes that room from the picker and selects one that fits. Any change reprices the quote. None of the widgets knows the others exist.

```ts title="booking.ts"
interface Room { code: string; capacity: number; nightlyCents: number }

type PanelEvent = "guests" | "nights" | "room";

interface PanelMediator {
  changed(event: PanelEvent): void;
}

class NumberInput {
  constructor(private readonly mediator: PanelMediator, private readonly event: PanelEvent, public value: number) {}
  set(value: number): void {
    this.value = value;
    this.mediator.changed(this.event);
  }
}

class RoomSelect {
  options: Room[] = [];
  selected?: Room;
  constructor(private readonly mediator: PanelMediator) {}
  choose(code: string): void {
    this.selected = this.options.find((r) => r.code === code) ?? this.selected;
    this.mediator.changed("room");
  }
  // Called by the mediator. It does not notify back, which prevents loops.
  limitTo(rooms: Room[]): void {
    this.options = rooms;
    if (!this.selected || !rooms.includes(this.selected)) this.selected = rooms[0];
  }
}

class QuoteLabel {
  text = "";
  show(text: string): void { this.text = text; }
}

class BookingPanel implements PanelMediator {
  readonly guests = new NumberInput(this, "guests", 2);
  readonly nights = new NumberInput(this, "nights", 1);
  readonly rooms = new RoomSelect(this);
  readonly quote = new QuoteLabel();

  constructor(private readonly catalog: Room[]) {
    this.changed("guests");
  }

  // Every cross-widget rule lives here.
  changed(event: PanelEvent): void {
    if (event === "guests") {
      this.rooms.limitTo(this.catalog.filter((r) => r.capacity >= this.guests.value));
    }
    const room = this.rooms.selected;
    const nights = this.nights.value;
    this.quote.show(room ? `${room.code} x${nights}: $${((room.nightlyCents * nights) / 100).toFixed(2)}` : "No room fits this party");
  }
}

const panel = new BookingPanel([
  { code: "STD", capacity: 2, nightlyCents: 8900 },
  { code: "DBL", capacity: 2, nightlyCents: 11900 },
  { code: "FAM", capacity: 4, nightlyCents: 18900 },
]);
panel.rooms.choose("DBL");
panel.nights.set(3);
console.log(panel.quote.text); // DBL x3: $357.00
panel.guests.set(4);
console.log(panel.quote.text); // FAM x3: $567.00
panel.guests.set(6);
console.log(panel.quote.text); // No room fits this party
```

```python title="booking.py"
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Protocol

@dataclass(frozen=True)
class Room:
    code: str
    capacity: int
    nightly_cents: int

class PanelMediator(Protocol):
    def changed(self, event: str) -> None: ...

class NumberInput:
    def __init__(self, mediator: PanelMediator, event: str, value: int) -> None:
        self._mediator, self._event, self.value = mediator, event, value

    def set(self, value: int) -> None:
        self.value = value
        self._mediator.changed(self._event)

class RoomSelect:
    def __init__(self, mediator: PanelMediator) -> None:
        self._mediator = mediator
        self.options: list[Room] = []
        self.selected: Optional[Room] = None

    def choose(self, code: str) -> None:
        self.selected = next((r for r in self.options if r.code == code), self.selected)
        self._mediator.changed("room")

    def limit_to(self, rooms: list[Room]) -> None:
        """Called by the mediator. Does not notify back, which prevents loops."""
        self.options = rooms
        if self.selected not in rooms:
            self.selected = rooms[0] if rooms else None

class QuoteLabel:
    def __init__(self) -> None:
        self.text = ""

    def show(self, text: str) -> None:
        self.text = text

class BookingPanel:
    def __init__(self, catalog: list[Room]) -> None:
        self._catalog = catalog
        self.guests = NumberInput(self, "guests", 2)
        self.nights = NumberInput(self, "nights", 1)
        self.rooms = RoomSelect(self)
        self.quote = QuoteLabel()
        self.changed("guests")

    def changed(self, event: str) -> None:
        if event == "guests":
            self.rooms.limit_to([r for r in self._catalog if r.capacity >= self.guests.value])
        room, nights = self.rooms.selected, self.nights.value
        if room is None:
            self.quote.show("No room fits this party")
        else:
            self.quote.show(f"{room.code} x{nights}: ${room.nightly_cents * nights / 100:.2f}")

if __name__ == "__main__":
    panel = BookingPanel([Room("STD", 2, 8900), Room("DBL", 2, 11900), Room("FAM", 4, 18900)])
    panel.rooms.choose("DBL")
    panel.nights.set(3)
    print(panel.quote.text)  # DBL x3: $357.00
    panel.guests.set(4)
    print(panel.quote.text)  # FAM x3: $567.00
    panel.guests.set(6)
    print(panel.quote.text)  # No room fits this party
```

```go title="booking.go"
package main

import (
	"fmt"
	"slices"
)

type Room struct {
	Code                   string
	Capacity, NightlyCents int
}

type PanelMediator interface{ Changed(event string) }

type NumberInput struct {
	mediator PanelMediator
	event    string
	Value    int
}

func (n *NumberInput) Set(v int) { n.Value = v; n.mediator.Changed(n.event) }

type RoomSelect struct {
	mediator PanelMediator
	options  []Room
	Selected *Room
}

func (s *RoomSelect) Choose(code string) {
	for i := range s.options {
		if s.options[i].Code == code {
			s.Selected = &s.options[i]
		}
	}
	s.mediator.Changed("room")
}

// LimitTo is called by the mediator. It does not notify back, which prevents loops.
func (s *RoomSelect) LimitTo(rooms []Room) {
	keep := s.Selected
	s.options, s.Selected = rooms, nil
	for i := range rooms { // keep the current room if it still fits, else take the first
		if s.Selected == nil || (keep != nil && rooms[i].Code == keep.Code) {
			s.Selected = &rooms[i]
		}
	}
}

type QuoteLabel struct{ Text string }

func (q *QuoteLabel) Show(text string) { q.Text = text }

type BookingPanel struct {
	catalog        []Room
	Guests, Nights *NumberInput
	Rooms          *RoomSelect
	Quote          *QuoteLabel
}

func NewBookingPanel(catalog []Room) *BookingPanel {
	p := &BookingPanel{catalog: catalog, Quote: &QuoteLabel{}}
	p.Guests = &NumberInput{mediator: p, event: "guests", Value: 2}
	p.Nights = &NumberInput{mediator: p, event: "nights", Value: 1}
	p.Rooms = &RoomSelect{mediator: p}
	p.Changed("guests")
	return p
}

func (p *BookingPanel) Changed(event string) {
	if event == "guests" {
		tooSmall := func(r Room) bool { return r.Capacity < p.Guests.Value }
		p.Rooms.LimitTo(slices.DeleteFunc(slices.Clone(p.catalog), tooSmall))
	}
	quote := "No room fits this party"
	if r, n := p.Rooms.Selected, p.Nights.Value; r != nil {
		quote = fmt.Sprintf("%s x%d: $%.2f", r.Code, n, float64(r.NightlyCents*n)/100)
	}
	p.Quote.Show(quote)
}

func main() {
	panel := NewBookingPanel([]Room{{"STD", 2, 8900}, {"DBL", 2, 11900}, {"FAM", 4, 18900}})
	panel.Rooms.Choose("DBL")
	panel.Nights.Set(3)
	fmt.Println(panel.Quote.Text) // DBL x3: $357.00
	panel.Guests.Set(4)
	fmt.Println(panel.Quote.Text) // FAM x3: $567.00
	panel.Guests.Set(6)
	fmt.Println(panel.Quote.Text) // No room fits this party
}
```

```java title="BookingPanelDemo.java"
import java.util.List;

public class BookingPanelDemo {
    record Room(String code, int capacity, long nightlyCents) {}

    interface PanelMediator { void changed(String event); }

    static final class NumberInput {
        private final PanelMediator mediator;
        private final String event;
        int value;

        NumberInput(PanelMediator mediator, String event, int value) {
            this.mediator = mediator; this.event = event; this.value = value;
        }

        void set(int value) { this.value = value; mediator.changed(event); }
    }

    static final class RoomSelect {
        private final PanelMediator mediator;
        List<Room> options = List.of();
        Room selected;

        RoomSelect(PanelMediator mediator) { this.mediator = mediator; }

        void choose(String code) {
            options.stream().filter(r -> r.code().equals(code)).findFirst().ifPresent(r -> selected = r);
            mediator.changed("room");
        }

        // Called by the mediator. It does not notify back, which prevents loops.
        void limitTo(List<Room> rooms) {
            options = rooms;
            if (selected == null || !rooms.contains(selected)) selected = rooms.isEmpty() ? null : rooms.get(0);
        }
    }

    static final class QuoteLabel {
        String text = "";
        void show(String text) { this.text = text; }
    }

    static final class BookingPanel implements PanelMediator {
        private final List<Room> catalog;
        final NumberInput guests = new NumberInput(this, "guests", 2);
        final NumberInput nights = new NumberInput(this, "nights", 1);
        final RoomSelect rooms = new RoomSelect(this);
        final QuoteLabel quote = new QuoteLabel();

        BookingPanel(List<Room> catalog) { this.catalog = catalog; changed("guests"); }

        public void changed(String event) {
            if (event.equals("guests")) {
                rooms.limitTo(catalog.stream().filter(r -> r.capacity() >= guests.value).toList());
            }
            Room room = rooms.selected;
            int n = nights.value;
            quote.show(room == null ? "No room fits this party"
                : String.format("%s x%d: $%.2f", room.code(), n, room.nightlyCents() * n / 100.0));
        }
    }

    public static void main(String[] args) {
        BookingPanel panel = new BookingPanel(List.of(
            new Room("STD", 2, 8900), new Room("DBL", 2, 11900), new Room("FAM", 4, 18900)));
        panel.rooms.choose("DBL");
        panel.nights.set(3);
        System.out.println(panel.quote.text); // DBL x3: $357.00
        panel.guests.set(4);
        System.out.println(panel.quote.text); // FAM x3: $567.00
        panel.guests.set(6);
        System.out.println(panel.quote.text); // No room fits this party
    }
}
```

## Trade-offs

**Gains**
- Components are reusable: they depend on a one-method interface, not on their neighbors.
- Interaction rules live in one class you can read top to bottom and test directly.
- N×N links become N links to the mediator.
- Adding a component touches the mediator only.

**Costs**
- The mediator tends to absorb every rule and becomes a [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md).
- Control flow is indirect: to see what a click does, you read the mediator, not the widget.
- Notifications that trigger notifications can loop unless the mediator's own calls are silent.
- For two or three components, it is more structure than the problem has.

## Relations

- [Observer](https://aipatterns.dev/design-patterns/observer.md) is often how colleagues notify the mediator. Observer broadcasts to anyone; Mediator encodes who should react and how.
- [Facade](https://aipatterns.dev/design-patterns/facade.md) simplifies access to a subsystem from outside, and the subsystem doesn't know about it. A mediator is known to its colleagues and coordinates them.
- [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) spreads a decision across handlers. Mediator concentrates it in one object.
- [Command](https://aipatterns.dev/design-patterns/command.md) objects can be what colleagues send to the mediator, which makes the interactions loggable and undoable.

## Agent checklist

- [ ] At least three components have cross-dependencies today. Otherwise keep direct calls or a callback.
- [ ] If a framework store or parent component already owns the state, you derived props from it instead of adding a mediator.
- [ ] Colleagues reference only the mediator interface, never each other or the concrete mediator.
- [ ] Methods the mediator calls on colleagues do not notify back.
- [ ] The mediator serves one workflow. Tests cover each rule by changing one colleague and asserting the others.
