Concepts / Structural patterns / Bridge

Bridge

Separating an abstraction from its implementation so both can vary.

01The intuition

The problem it solves

A school app sends messages. There are kinds of message (a reminder, an urgent alert, a weekly digest) and ways to deliver them (SMS, email, push). The obvious design makes one class for every combination: ReminderSms, ReminderEmail, AlertSms, and so on. Three kinds times three channels is nine classes; add WhatsApp and you write three more; add a new kind and you write four more.

class ReminderSms:
    def send(self, to: str, body: str) -> None:
        sms_gateway.send(to, "Reminder: " + body)

class ReminderEmail:
    def send(self, to: str, body: str) -> None:
        smtp.send(to, subject="Reminder", text=body)

class AlertSms:
    def send(self, to: str, body: str) -> None:
        sms_gateway.send(to, "URGENT: " + body)

class AlertEmail:
    def send(self, to: str, body: str) -> None:
        smtp.send(to, subject="URGENT", text=body)
# ... five more, and each new channel multiplies the list

Build it one piece at a time

Step one: notice there are two things changing for different reasons. What the message is (its wording and priority) and how it travels. Give the second one its own small interface with one job.

from typing import Protocol

class Channel(Protocol):
    def deliver(self, to: str, subject: str, text: str) -> None: ...

class Sms:
    def deliver(self, to: str, subject: str, text: str) -> None:
        sms_gateway.send(to, f"{subject}: {text}")

class Email:
    def deliver(self, to: str, subject: str, text: str) -> None:
        smtp.send(to, subject=subject, text=text)

Step two: let each message kind hold a channel instead of being one. The message decides the words; it hands the result to whatever channel it was given. The line between the two hierarchies is the bridge.

class Message:
    def __init__(self, channel: Channel) -> None:
        self.channel = channel
    def send(self, to: str, body: str) -> None:
        self.channel.deliver(to, self.subject(), self.text(body))
    def subject(self) -> str: raise NotImplementedError
    def text(self, body: str) -> str: raise NotImplementedError

class Reminder(Message):
    def subject(self) -> str: return "Reminder"
    def text(self, body: str) -> str: return body

class Alert(Message):
    def subject(self) -> str: return "URGENT"
    def text(self, body: str) -> str: return body.upper()

Step three: combine at runtime. Three kinds and three channels are now six classes, and any pair works. The caller picks a kind and a channel in one line, and the two choices never need to know about each other. A test for the wording of an alert can use a fake channel that just records what it was given, which was impossible when the wording and the gateway call lived in one method.

Alert(Sms()).send("+91...", "school closed tomorrow")
Reminder(Email()).send("parent@example.com", "fees due Friday")

class Push:                         # new channel: one class, zero edits elsewhere
    def deliver(self, to: str, subject: str, text: str) -> None:
        fcm.push(to, title=subject, body=text)

Why it works

The principle: when two independent things vary, do not multiply classes, compose them. The abstraction (message kinds) and the implementation (channels) each get their own hierarchy, and the abstraction holds a reference to an implementation. The invariant is that the abstraction never knows which concrete channel it holds; it only uses the Channel promise. Where it breaks: a message class that checks isinstance(self.channel, Sms) to shorten its text has rebuilt the grid by hand. The interviewer's test is "add a fourth channel and a fourth kind; how many classes do you write?" The answer should be two.

The trade-off you must say out loud

You have two hierarchies to read instead of one, and the channel interface must be general enough for every kind, which can leave some methods awkward (SMS has no subject). If only one of the axes ever changes, a plain strategy object is enough and the word bridge is overkill. The follow-up is often how this differs from Strategy: the shapes look alike, but a strategy swaps one algorithm inside one class, while a bridge keeps two whole families independent.

Where it shows up in the 100 Google problems

  • Notification service: message templates on one side, channels on the other, combined per user preference.
  • Logging framework: log levels and formatting vary independently of the sinks (console, file, network).
  • Multi-elevator dispatcher: the dispatch policy family (nearest car, SCAN, zoning) is bridged from the car-control family (door timing, acceleration).
  • Music player: playback controls are the abstraction; local files, streamed audio and Bluetooth output are the implementations.

02Used in these problems