Decouple an abstraction from its implementation so that the two can vary independently — avoiding a permanent binding between them.
The Bridge pattern separates a class into two independent hierarchies — the abstraction (what it does) and the implementation (how it does it) — connected by a reference (the bridge). Both can evolve independently without a combinatorial explosion of subclasses.
Imagine a notification system: you have notification types (Alert, Reminder, Welcome) and delivery channels (Email, SMS, Slack). Without Bridge, you'd need AlertEmail, AlertSMS, AlertSlack, ReminderEmail, ReminderSMS... 3×3 = 9 classes. Add one type or channel and it doubles. With Bridge: 3 + 3 = 6 classes total, any combination.
The key insight: use composition instead of inheritance to connect the two hierarchies. The abstraction holds a reference to an implementor and delegates platform-specific work to it.
Key Idea: Split one large class hierarchy into two smaller ones — abstraction and implementation — and connect them with a reference. Change either side independently.
Without Bridge, adding dimensions to a class hierarchy causes class count to multiply:
// Without Bridge — 3 types × 3 channels = 9 classes
class AlertEmailNotification {}
class AlertSMSNotification {}
class AlertSlackNotification {}
class ReminderEmailNotification {}
class ReminderSMSNotification {}
class ReminderSlackNotification {}
class WelcomeEmailNotification {}
class WelcomeSMSNotification {}
class WelcomeSlackNotification {}
// Add 1 new type → 3 more classes. Add 1 new channel → 3 more classes.
// With Bridge — 3 types + 3 senders = 6 classes, infinite combos
class AlertNotification { constructor(sender) {} }
class ReminderNotification { constructor(sender) {} }
class WelcomeNotification { constructor(sender) {} }
class EmailSender {}
class SMSSender {}
class SlackSender {}
// new AlertNotification(new SMSSender()) — zero new classes!Rule of thumb: If you see a class hierarchy growing in two independent dimensions simultaneously, Bridge is likely the solution.
The high-level control layer. Defines the interface the client uses. Holds a reference to an Implementor (the bridge). Delegates platform-specific work to the implementor.
Extends the Abstraction with more specific behaviour. Still delegates to the implementor — doesn't care which concrete implementor it holds.
Defines the interface for the implementation layer. Typically lower-level operations that the Abstraction composes into higher-level operations.
Platform-specific implementations of the Implementor interface — the actual "how". Each provides a different backend, channel, or technology.
// Bridge Pattern — Notification System
// ─── Implementor interface ────────────────────────────────────
interface MessageSender {
void send(String recipient, String subject, String body);
}
// ─── Concrete Implementors ────────────────────────────────────
class EmailSender implements MessageSender {
public void send(String recipient, String subject, String body) {
System.out.println("[Email] To: " + recipient + " | Subject: " + subject + " | " + body);
}
}
class SMSSender implements MessageSender {
public void send(String recipient, String subject, String body) {
// SMS has no subject, truncate body
System.out.println("[SMS] To: " + recipient + " | " + body.substring(0, Math.min(160, body.length())));
}
}
class SlackSender implements MessageSender {
public void send(String recipient, String subject, String body) {
System.out.println("[Slack] Channel: #" + recipient + " | *" + subject + "* " + body);
}
}
// ─── Abstraction ──────────────────────────────────────────────
abstract class Notification {
protected MessageSender sender; // the bridge
public Notification(MessageSender sender) { this.sender = sender; }
public abstract void notify(String recipient, String message);
}
// ─── Refined Abstractions ─────────────────────────────────────
class AlertNotification extends Notification {
public AlertNotification(MessageSender sender) { super(sender); }
public void notify(String recipient, String message) {
sender.send(recipient, "🚨 ALERT", "[URGENT] " + message);
}
}
class ReminderNotification extends Notification {
public ReminderNotification(MessageSender sender) { super(sender); }
public void notify(String recipient, String message) {
sender.send(recipient, "⏰ Reminder", message);
}
}
class WelcomeNotification extends Notification {
public WelcomeNotification(MessageSender sender) { super(sender); }
public void notify(String recipient, String message) {
sender.send(recipient, "👋 Welcome!", "Hi " + recipient + "! " + message);
}
}
// ─── Client ───────────────────────────────────────────────────
public class App {
public static void main(String[] args) {
// Mix and match abstraction × implementation freely
Notification emailAlert = new AlertNotification(new EmailSender());
Notification smsAlert = new AlertNotification(new SMSSender());
Notification slackRemind = new ReminderNotification(new SlackSender());
Notification emailWelcome = new WelcomeNotification(new EmailSender());
emailAlert.notify("admin@example.com", "Server CPU at 98%!");
smsAlert.notify("+91-9876543210", "Server CPU at 98%!");
slackRemind.notify("devops", "Deployment at 3pm today");
emailWelcome.notify("alice", "Your account is ready.");
// 3 types × 3 senders = 9 combos — zero new classes needed!
}
}When a class hierarchy grows in two directions at once — shape+renderer, notification+channel, exporter+format — Bridge prevents the cartesian explosion.
Shape+Renderer, Notification+ChannelWhen you need to swap the underlying implementation at runtime without changing the abstraction — e.g., switch from local to remote storage without changing the business layer.
StorageBridge, PaymentBridgeWhen the same abstraction needs multiple platform implementations — the abstraction stays the same, concrete implementors handle platform specifics.
DrawingAPI, DatabaseDriver, UIToolkitJDBC is a textbook Bridge: Connection, Statement, ResultSet are the abstraction. MySQL driver, PostgreSQL driver, Oracle driver are the concrete implementors. Your code uses JDBC API regardless of the database.
SLF4J is the abstraction (Logger interface). Logback, Log4j, and java.util.logging are the concrete implementors. Switch the logging backend by swapping the JAR — application code unchanged.
Python's logging.Logger is the abstraction. StreamHandler, FileHandler, SMTPHandler, SocketHandler are implementors. You can add multiple handlers to a single logger at runtime.
React's rendering pipeline is a Bridge: the reconciliation algorithm (abstraction) is separate from the renderer (implementor). React DOM, React Native, and React Three Fiber are all different implementors of the same reconciler.