Design Patterns, Explained Simply · Structural Patterns: Fitting Objects Together

Bridge

Split one large hierarchy into two independent ones, what something is and how it's done, and connect them with composition so they can grow separately.

The problem

A notification system has message types (alerts and reminders) and delivery channels (email and SMS). The first design used inheritance: EmailAlert, SmsAlert, EmailReminder, SmsReminder.

Product then asks for push notifications and a "digest" message type. That is 3 types × 3 channels = 9 classes, mostly copy-pasted, and every new channel or type multiplies the count again.

Real-world analogy

A TV remote and the TV are separate. The remote (the "what": volume up, next channel) talks to the TV (the "how") through a standard signal. Any remote model works with any TV brand, and you can upgrade either one without replacing the other.

Without the pattern

Message
├── EmailAlert      ├── SmsAlert      ├── PushAlert
├── EmailReminder   ├── SmsReminder   ├── PushReminder
└── EmailDigest     └── SmsDigest     └── PushDigest      (9 classes, growing M × N)

The pattern

  • Abstraction (Message): the high-level "what". It holds a reference to an implementation (that reference is the bridge).
  • Refined abstractions (Alert, Reminder): variations of the "what".
  • Implementation interface (Channel): the low-level "how", with simple operations like send(to, text).
  • Concrete implementations (EmailChannel, SmsChannel).

Both sides grow independently: M message types + N channels = M + N classes instead of M × N.

The code

Message types and channels varying independentlyTry it yourself
class EmailChannel:  # implementation side: HOW to deliver
    def send(self, to, text):
        return f"email to {to}: {text}"


class SmsChannel:
    def send(self, to, text):
        return f"sms to {to}: {text[:30]}"  # SMS keeps it short


class Message:  # abstraction side: WHAT to say
    def __init__(self, channel):
        self.channel = channel  # the bridge


class Alert(Message):
    def notify(self, to, event):
        return self.channel.send(to, f"ALERT: {event} needs attention now")


class Reminder(Message):
    def notify(self, to, event):
        return self.channel.send(to, f"Reminder: {event} is tomorrow")


for message in [Alert(EmailChannel()), Alert(SmsChannel()),
                Reminder(EmailChannel()), Reminder(SmsChannel())]:
    print(message.notify("ada", "database backup"))

Output

email to ada: ALERT: database backup needs attention now
sms to ada: ALERT: database backup needs a
email to ada: Reminder: database backup is tomorrow
sms to ada: Reminder: database backup is t

Adding push notifications is one PushChannel class; adding a digest is one Digest class. Every combination works automatically.

When to use it

  • A class varies in two independent dimensions (what × how, shape × renderer, message × channel, report × export format) and inheritance would multiply classes.
  • You want to switch the implementation at runtime (pick a channel per user preference).
  • You want to develop the two sides separately, for example a platform team owning channels and a product team owning message types.

When not to use it

  • There is only one dimension of variation. Strategy or a simple interface is enough.
  • The two sides are tightly linked and never vary independently.
  • It is often confused with Adapter. Adapter makes existing, unrelated interfaces work together after the fact; Bridge is designed up front to let two hierarchies vary independently.

You've already used it

  • JDBC: your code uses the JDBC API (abstraction) while each driver implements the vendor-specific part (implementation).
  • Logging facades: SLF4J or PSR-3 loggers (abstraction) with interchangeable handlers that write to files, syslog or the cloud (implementation).
  • Graphics toolkits: shapes drawn through a renderer interface that has OpenGL, Vulkan or software implementations.

Key points

  • Bridge splits a class into abstraction (what) and implementation (how), linked by composition.
  • It turns M × N subclasses into M + N classes.
  • Both sides can grow and change independently, even at runtime.
  • Further reading: Refactoring Guru: Bridge.

Practice questions

What problem does the Bridge pattern solve?

Answer

The class explosion when something varies in two independent dimensions (message type × channel). Bridge separates the abstraction (what) from the implementation (how) and connects them by composition, so M + N classes cover every combination instead of M × N.

What is the difference between Bridge and Adapter?

Answer

Adapter is applied after the fact to make an existing class with an incompatible interface fit. Bridge is designed up front so that two hierarchies (abstraction and implementation) can vary independently. Structurally both use composition, but their intent differs.

Give a real-world example of Bridge.

Answer

JDBC: application code uses the JDBC API while vendor drivers provide the implementation. Logging facades with pluggable handlers (SLF4J, PSR-3 with Monolog handlers) follow the same idea.

Open in the app to mark lessons complete, take notes and track your progress.