InterviewKata
Design patterns · 11 of 26 · Fitting objects together

Bridge

Bridge separates two things that vary independently, so each can grow without multiplying classes. This page splits Pedal's operations alerts (what to say) from delivery channels (how to send it), and counts the classes the split saves.

1. The pain

Pedal's operations team gets alerts: a bike's battery is low, a bike moved without a ride (probably stolen), a dock's lock jammed. Each can go out by SMS, phone push or email, depending on who's on shift. The first version made a class for every combination: TheftSms, TheftPush, LowBatteryEmail…

Three alerts and three channels made nine classes. Changing how SMS marks urgency meant editing three of them. Adding a pager channel meant three more, and the next alert kind would mean four. Two independent things were glued into one class hierarchy, so it grew as a product instead of a sum.

2. The idea in one picture

A TV remote and the TV are made separately. Any remote that speaks the standard can drive any TV that understands it: a basic remote, a universal one, or the phone app. Brands of TV and kinds of remote evolve on their own, and nobody makes a separate remote for every model of TV.

Bridge splits one hierarchy into two that vary independently, joined by composition. The abstraction (what an alert says, and whether it's urgent) holds a reference to an implementation (how a message travels). Theft decides its text and that it's urgent; Push decides that urgent means "priority high". Either side can grow without touching the other.

Bridge is usually designed up front, when you can see two dimensions of variation coming. Adapter, which it resembles, is usually added later to make existing pieces fit.

3. The roles

RoleIts jobIn Pedal
AbstractionThe high-level side; holds an implementor and delegates the low-level work to it.Alert with send()
Refined abstractionVariants of the high-level side.LowBattery, Theft, DockFault
ImplementorThe interface for the low-level side.Channel.deliver(to, text, urgent)
Concrete implementorOne way of doing the low-level work.Sms, Push, Email

4. Before and after

The alert classes both ways (Python sketch; the full program is in section 5).

Without the pattern

class TheftSms(Alert): ...
class TheftPush(Alert): ...
class TheftEmail(Alert): ...
class LowBatterySms(Alert): ...
class LowBatteryPush(Alert): ...
class LowBatteryEmail(Alert): ...
class DockFaultSms(Alert): ...
class DockFaultPush(Alert): ...
class DockFaultEmail(Alert): ...
# a pager channel: three more classes

With it

class Alert(ABC):
    def __init__(self, channel, to):
        self.channel, self.to = channel, to

    def send(self):
        return self.channel.deliver(
            self.to, self.text(), self.urgent())

Theft(Push(), "ops-app", "B-17").send()
Theft(Sms(), phone, "B-17").send()
The next changeWithout the patternWith it
Add a pager channelOne class per alert kind (3)One Pager class
Change how SMS marks urgencyEdit every …Sms class (3)Edit Sms.deliver
Route an alert by shift (SMS at night)Pick a different class at runtimePass a different channel

Two dimensions in two hierarchies grow as a sum (alerts + channels), not a product (alerts × channels).

5. The code

Three alert kinds hold one of three channels. The same theft alert goes out by push and by SMS, and each channel shows urgency in its own way.

from __future__ import annotations
from abc import ABC, abstractmethod


# ---- One side of the bridge: how a message travels. ----
class Channel(ABC):
    @abstractmethod
    def deliver(self, to: str, text: str, urgent: bool) -> str: ...


class Sms(Channel):
    def deliver(self, to: str, text: str, urgent: bool) -> str:
        return f"sms {to}: {'URGENT ' if urgent else ''}{text}"


class Push(Channel):
    def deliver(self, to: str, text: str, urgent: bool) -> str:
        return f"push {to} [priority {'high' if urgent else 'normal'}]: {text}"


class Email(Channel):
    def deliver(self, to: str, text: str, urgent: bool) -> str:
        return f"email {to}: {'[URGENT] ' if urgent else ''}{text}"


# ---- The other side: what is being said. Each alert holds a channel instead of being one. ----
class Alert(ABC):
    def __init__(self, channel: Channel, to: str) -> None:
        self.channel, self.to = channel, to

    def send(self) -> str:
        return f"{self.title()} -> " + self.channel.deliver(self.to, self.text(), self.urgent())

    @abstractmethod
    def title(self) -> str: ...

    @abstractmethod
    def text(self) -> str: ...

    @abstractmethod
    def urgent(self) -> bool: ...


class LowBattery(Alert):
    def __init__(self, channel: Channel, to: str, bike: str, percent: int) -> None:
        super().__init__(channel, to)
        self.bike, self.percent = bike, percent

    def title(self) -> str: return f"low battery {self.bike}"
    def text(self) -> str: return f"{self.bike} battery at {self.percent}%"
    def urgent(self) -> bool: return self.percent < 15


class Theft(Alert):
    def __init__(self, channel: Channel, to: str, bike: str) -> None:
        super().__init__(channel, to)
        self.bike = bike

    def title(self) -> str: return f"theft {self.bike}"
    def text(self) -> str: return f"{self.bike} moved without a ride"
    def urgent(self) -> bool: return True


class DockFault(Alert):
    def __init__(self, channel: Channel, to: str, dock: str, problem: str) -> None:
        super().__init__(channel, to)
        self.dock, self.problem = dock, problem

    def title(self) -> str: return f"dock fault {self.dock}"
    def text(self) -> str: return f"{self.dock} {self.problem}"
    def urgent(self) -> bool: return False


if __name__ == "__main__":
    phone, app, inbox = "+44 7700 900123", "ops-app", "ops@pedal.example"
    alerts: list[Alert] = [
        LowBattery(Sms(), phone, "E-205", 31),
        Theft(Push(), app, "B-17"),
        DockFault(Email(), inbox, "D9", "lock jammed"),
        Theft(Sms(), phone, "B-17"),                 # same alert, another channel
        LowBattery(Push(), app, "E-311", 9),         # same channel, urgency decided by the alert
    ]
    for a in alerts:
        print(a.send())
    print("classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)")
// ---- One side of the bridge: how a message travels. ----
interface Channel { deliver(to: string, text: string, urgent: boolean): string; }

class Sms implements Channel {
  deliver(to: string, text: string, urgent: boolean) { return `sms ${to}: ${urgent ? "URGENT " : ""}${text}`; }
}
class Push implements Channel {
  deliver(to: string, text: string, urgent: boolean) { return `push ${to} [priority ${urgent ? "high" : "normal"}]: ${text}`; }
}
class Email implements Channel {
  deliver(to: string, text: string, urgent: boolean) { return `email ${to}: ${urgent ? "[URGENT] " : ""}${text}`; }
}

// ---- The other side: what is being said. Each alert holds a channel instead of being one. ----
abstract class Alert {
  constructor(private readonly channel: Channel, private readonly to: string) {}
  send(): string { return `${this.title()} -> ` + this.channel.deliver(this.to, this.text(), this.urgent()); }
  abstract title(): string;
  abstract text(): string;
  abstract urgent(): boolean;
}

class LowBattery extends Alert {
  constructor(channel: Channel, to: string, private readonly bike: string, private readonly percent: number) { super(channel, to); }
  title() { return `low battery ${this.bike}`; }
  text() { return `${this.bike} battery at ${this.percent}%`; }
  urgent() { return this.percent < 15; }
}

class Theft extends Alert {
  constructor(channel: Channel, to: string, private readonly bike: string) { super(channel, to); }
  title() { return `theft ${this.bike}`; }
  text() { return `${this.bike} moved without a ride`; }
  urgent() { return true; }
}

class DockFault extends Alert {
  constructor(channel: Channel, to: string, private readonly dock: string, private readonly problem: string) { super(channel, to); }
  title() { return `dock fault ${this.dock}`; }
  text() { return `${this.dock} ${this.problem}`; }
  urgent() { return false; }
}

const phone = "+44 7700 900123", app = "ops-app", inbox = "ops@pedal.example";
const alerts: Alert[] = [
  new LowBattery(new Sms(), phone, "E-205", 31),
  new Theft(new Push(), app, "B-17"),
  new DockFault(new Email(), inbox, "D9", "lock jammed"),
  new Theft(new Sms(), phone, "B-17"),            // same alert, another channel
  new LowBattery(new Push(), app, "E-311", 9),    // same channel, urgency decided by the alert
];
for (const a of alerts) console.log(a.send());
console.log("classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)");
import java.util.List;

public class Main {
    // ---- One side of the bridge: how a message travels. ----
    interface Channel { String deliver(String to, String text, boolean urgent); }

    static class Sms implements Channel {
        public String deliver(String to, String text, boolean urgent) { return "sms " + to + ": " + (urgent ? "URGENT " : "") + text; }
    }
    static class Push implements Channel {
        public String deliver(String to, String text, boolean urgent) { return "push " + to + " [priority " + (urgent ? "high" : "normal") + "]: " + text; }
    }
    static class Email implements Channel {
        public String deliver(String to, String text, boolean urgent) { return "email " + to + ": " + (urgent ? "[URGENT] " : "") + text; }
    }

    // ---- The other side: what is being said. Each alert holds a channel instead of being one. ----
    abstract static class Alert {
        private final Channel channel;
        private final String to;
        Alert(Channel channel, String to) { this.channel = channel; this.to = to; }
        String send() { return title() + " -> " + channel.deliver(to, text(), urgent()); }
        abstract String title();
        abstract String text();
        abstract boolean urgent();
    }

    static class LowBattery extends Alert {
        private final String bike; private final int percent;
        LowBattery(Channel c, String to, String bike, int percent) { super(c, to); this.bike = bike; this.percent = percent; }
        String title() { return "low battery " + bike; }
        String text() { return bike + " battery at " + percent + "%"; }
        boolean urgent() { return percent < 15; }
    }

    static class Theft extends Alert {
        private final String bike;
        Theft(Channel c, String to, String bike) { super(c, to); this.bike = bike; }
        String title() { return "theft " + bike; }
        String text() { return bike + " moved without a ride"; }
        boolean urgent() { return true; }
    }

    static class DockFault extends Alert {
        private final String dock, problem;
        DockFault(Channel c, String to, String dock, String problem) { super(c, to); this.dock = dock; this.problem = problem; }
        String title() { return "dock fault " + dock; }
        String text() { return dock + " " + problem; }
        boolean urgent() { return false; }
    }

    public static void main(String[] args) {
        String phone = "+44 7700 900123", app = "ops-app", inbox = "ops@pedal.example";
        List<Alert> alerts = List.of(
            new LowBattery(new Sms(), phone, "E-205", 31),
            new Theft(new Push(), app, "B-17"),
            new DockFault(new Email(), inbox, "D9", "lock jammed"),
            new Theft(new Sms(), phone, "B-17"),            // same alert, another channel
            new LowBattery(new Push(), app, "E-311", 9));   // same channel, urgency decided by the alert
        for (Alert a : alerts) System.out.println(a.send());
        System.out.println("classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)");
    }
}

Output (the same in all three languages)

low battery E-205 -> sms +44 7700 900123: E-205 battery at 31%
theft B-17 -> push ops-app [priority high]: B-17 moved without a ride
dock fault D9 -> email ops@pedal.example: D9 lock jammed
theft B-17 -> sms +44 7700 900123: URGENT B-17 moved without a ride
low battery E-311 -> push ops-app [priority high]: E-311 battery at 9%
classes: 3 alerts + 3 channels = 6 (one class per pair would need 9)

6. Try it

Grow both sides

Add alert kinds and channels and compare how many classes each design needs.

Each cell is one combination people can use. Without a bridge each cell is a class; with one, only the row and column headers are.

7. When not to use it

What you get

  • Linear growth: alerts + channels classes, not alerts × channels.
  • Independent change: edit one channel without touching any alert.
  • Runtime choice: the same alert can travel by whichever channel suits the shift.
  • Hides the platform: alerts don't know SMS gateways exist.

What you pay

  • Up-front design: you must spot the two dimensions early.
  • An interface to agree: deliver(to, text, urgent) must suit every alert and channel.
  • Indirection: the behaviour of one alert is split across two classes.
  • Overkill for one dimension: if channels never vary, it's just extra code.
If…Use instead
Only one thing varies13 Strategy or plain subclasses
You're fitting an existing class with the wrong interface06 Adapter
Several channels must receive the same alertSend to a list, or use 18 Observer
The pairs genuinely differ in logicSeparate classes may be honest

8. How it goes wrong

An implementor interface shaped by one sideIf deliver() takes a battery_percent, only battery alerts fit. Keep it about delivery.
Channels that know alert kindsif isinstance(alert, Theft) inside Sms glues the two sides back together.
Bridging a single dimensionWith one channel and no plans for more, the split adds classes and nothing else.
Confusing it with strategyA strategy swaps one algorithm inside an object; a bridge separates a whole family of abstractions from a family of implementations.

9. Remember it and say it

Memory hook: "Any remote, any TV"What to say and how to send it vary separately, joined by one agreed interface.
How this hook works: what “Any remote, any TV” means

What the slogan means, phrase by phrase

“Any remote”
Any alert kind (the abstraction).
“any TV”
Any channel (the implementation).
“one agreed interface”
Channel.deliver().

Why this picture fits: Remotes and TVs are made by different people on different schedules and still work together.

The story → the concept

The living roomThe code
The remoteTheft, LowBattery
The TVSms, Push, Email
The infra-red standarddeliver(to, text, urgent)
A new TV brandA Pager channel

Watch it on an example

  1. Theft decides the text and that it's urgent.
  2. It hands both to whichever channel it holds.
  3. Push shows urgency as "priority high"; Sms as an URGENT prefix.

The trap it saves you from

Building one class per combination, so the hierarchy grows as a product.

It still works when…

Yes: JDBC (application code over any database driver) and graphics toolkits (shapes over any rendering back end).

In an interview or code review

10. Check yourself

What problem does a bridge solve?

It splits them into two hierarchies.

4 alert kinds and 5 channels: classes without and with a bridge?

Product versus sum.

Where is "how urgency looks on SMS" decided?

The implementation side owns delivery details.

What joins the two hierarchies?

That's the bridge.

Bridge vs adapter?

Similar shapes, different moments and intents.

11. Go deeper

ResourceWhy
06 AdapterThe look-alike pattern, used after the fact.
13 StrategyOne varying algorithm, rather than two families.
Java DriverManager (JDBC)Application code on one side, any database driver on the other.
Wikipedia: Bridge patternStructure and examples.