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 likesend(to, text). - Concrete implementations (
EmailChannel,SmsChannel).
Both sides grow independently: M message types + N channels = M + N classes instead of M × N.
The code
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
interface Channel { // implementation side: HOW to deliver
send(to: string, text: string): string;
}
class EmailChannel implements Channel {
send(to: string, text: string) { return `email to ${to}: ${text}`; }
}
class SmsChannel implements Channel {
send(to: string, text: string) { return `sms to ${to}: ${text.slice(0, 30)}`; } // SMS keeps it short
}
abstract class Message { // abstraction side: WHAT to say
constructor(protected channel: Channel) {} // the bridge
abstract notify(to: string, event: string): string;
}
class Alert extends Message {
notify(to: string, event: string) { return this.channel.send(to, `ALERT: ${event} needs attention now`); }
}
class Reminder extends Message {
notify(to: string, event: string) { return this.channel.send(to, `Reminder: ${event} is tomorrow`); }
}
const messages: Message[] = [new Alert(new EmailChannel()), new Alert(new SmsChannel()),
new Reminder(new EmailChannel()), new Reminder(new SmsChannel())];
for (const message of messages) console.log(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
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Message> messages = List.of(new Alert(new EmailChannel()), new Alert(new SmsChannel()),
new Reminder(new EmailChannel()), new Reminder(new SmsChannel()));
for (Message message : messages) System.out.println(message.notify("ada", "database backup"));
}
}
interface Channel { // implementation side: HOW to deliver
String send(String to, String text);
}
class EmailChannel implements Channel {
public String send(String to, String text) { return "email to " + to + ": " + text; }
}
class SmsChannel implements Channel {
public String send(String to, String text) { // SMS keeps it short
return "sms to " + to + ": " + text.substring(0, Math.min(30, text.length()));
}
}
abstract class Message { // abstraction side: WHAT to say
protected final Channel channel; // the bridge
Message(Channel channel) { this.channel = channel; }
abstract String notify(String to, String event);
}
class Alert extends Message {
Alert(Channel channel) { super(channel); }
String notify(String to, String event) { return channel.send(to, "ALERT: " + event + " needs attention now"); }
}
class Reminder extends Message {
Reminder(Channel channel) { super(channel); }
String notify(String to, String event) { return channel.send(to, "Reminder: " + event + " is tomorrow"); }
}
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
Run this on your computer — see “Get Started” for setup.
<?php
interface Channel { // implementation side: HOW to deliver
public function send(string $to, string $text): string;
}
class EmailChannel implements Channel {
public function send(string $to, string $text): string { return "email to $to: $text"; }
}
class SmsChannel implements Channel {
public function send(string $to, string $text): string { return "sms to $to: " . substr($text, 0, 30); } // SMS keeps it short
}
abstract class Message { // abstraction side: WHAT to say
public function __construct(protected Channel $channel) {} // the bridge
abstract public function notify(string $to, string $event): string;
}
class Alert extends Message {
public function notify(string $to, string $event): string {
return $this->channel->send($to, "ALERT: $event needs attention now");
}
}
class Reminder extends Message {
public function notify(string $to, string $event): string {
return $this->channel->send($to, "Reminder: $event is tomorrow");
}
}
$messages = [new Alert(new EmailChannel()), new Alert(new SmsChannel()),
new Reminder(new EmailChannel()), new Reminder(new SmsChannel())];
foreach ($messages as $message) echo $message->notify("ada", "database backup"), "\n";
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.