Bridge Pattern
Decouple an abstraction from its implementation so the two can vary independently — and avoid a class explosion.
Intent
Bridge splits a large class — or a set of closely related classes — into two separate hierarchies, abstraction and implementation, connected by composition. Each side can then be extended without touching the other.
The class explosion problem
Suppose you have notifications of different urgency (regular, urgent) delivered over different channels (email, SMS, Slack). With inheritance alone you need a class per combination:
RegularEmailNotification UrgentEmailNotification
RegularSmsNotification UrgentSmsNotification
RegularSlackNotification UrgentSlackNotification
Add a channel and you write two classes; add an urgency and you write three. The hierarchy grows as M × N. Bridge makes it M + N.
Structure
classDiagram
direction LR
class Notification {
<<abstract>>
#MessageSender sender
+notify(String msg)*
}
class RegularNotification
class UrgentNotification
class MessageSender {
<<interface>>
+send(String title, String body)
}
class EmailSender
class SmsSender
class SlackSender
Notification <|-- RegularNotification
Notification <|-- UrgentNotification
Notification o--> MessageSender : bridge
MessageSender <|.. EmailSender
MessageSender <|.. SmsSender
MessageSender <|.. SlackSender
Example
// ---- Implementation hierarchy: HOW a message is delivered ----
interface MessageSender {
void send(String title, String body);
}
class EmailSender implements MessageSender {
public void send(String title, String body) {
System.out.println("[email] Subject: " + title + " | " + body);
}
}
class SmsSender implements MessageSender {
public void send(String title, String body) {
System.out.println("[sms] " + (title + ": " + body).substring(0, Math.min(40, title.length() + body.length() + 2)));
}
}
class SlackSender implements MessageSender {
public void send(String title, String body) {
System.out.println("[slack] *" + title + "* " + body);
}
}
// ---- Abstraction hierarchy: WHAT kind of notification it is ----
abstract class Notification {
protected final MessageSender sender; // the bridge
Notification(MessageSender sender) { this.sender = sender; }
abstract void notify(String message);
}
class RegularNotification extends Notification {
RegularNotification(MessageSender s) { super(s); }
void notify(String message) { sender.send("Update", message); }
}
class UrgentNotification extends Notification {
UrgentNotification(MessageSender s) { super(s); }
void notify(String message) {
// urgent notifications retry and shout — independent of the channel
for (int attempt = 1; attempt <= 2; attempt++) {
sender.send("URGENT (attempt " + attempt + ")", message.toUpperCase());
}
}
}
class Main {
public static void main(String[] args) {
new RegularNotification(new EmailSender()).notify("Your invoice is ready");
new UrgentNotification(new SlackSender()).notify("Production database is down");
new UrgentNotification(new SmsSender()).notify("Card used abroad");
}
}Three channels × two urgencies = five classes instead of six — and the gap widens quickly: 5 channels × 4 urgencies is 9 classes instead of 20.
Where you’ll see it
- JDBC is the textbook Bridge: your code uses the
java.sqlabstraction (Connection,Statement); each vendor’s driver is an independent implementation hierarchy, selected at runtime byDriverManager. - SLF4J — the logging API is the abstraction; Logback, Log4j 2 and
java.util.loggingbindings are implementations. - AWT’s peer architecture:
java.awt.Buttondelegates to a platform-specificButtonPeer.
Bridge vs Strategy vs Adapter
| Bridge | Strategy | Adapter | |
|---|---|---|---|
| Designed | Up-front | Up-front | After the fact |
| Goal | Let two hierarchies vary independently | Swap an algorithm | Make incompatible interfaces fit |
| Structure | Abstraction hierarchy holds an implementor | Context holds one strategy | Wrapper translates calls |
Mark it complete to track your progress through Design Patterns.