Bridge Pattern
Decouple an abstraction from its implementation so that the two can vary independently.
Overview
Most class hierarchies start simple and grow complicated. You add a feature, then a variant of that feature, then a variant of the variant — and suddenly you have an explosion of subclasses, one for every combination of feature and implementation detail.
The Bridge pattern is a structural design pattern that cuts this combinatorial growth off at the root. It separates the high-level abstraction (the "what") from the low-level implementation (the "how") by placing them in two independent class hierarchies connected by a reference — the bridge.
The result is a design where you can add new abstractions or new implementations without modifying either side. The two hierarchies evolve in parallel.
Problem & Motivation
Suppose you are building a notification system. You have two notification types — AlertNotification (urgent, must be seen) and SummaryNotification (digest, nice to have) — and three delivery channels: Email, SMS, and Slack.
A naive subclass-per-combination design gives you six classes:
EmailAlertNotificationSMSAlertNotificationSlackAlertNotificationEmailSummaryNotificationSMSSummaryNotificationSlackSummaryNotification
Add a fourth channel (push notifications) and you add two more classes. Add a third notification type (reminder) and you add three more. The count grows as m × n where m is the number of abstractions and n is the number of implementations.
Bridge collapses this to m + n. The Notification hierarchy knows about urgency and formatting. The MessageSender hierarchy knows about delivery. Neither knows the internals of the other.
Class Diagram
Loading diagram...
The key structural points are:
Notificationis the abstraction. It holds a reference to aMessageSenderand delegates delivery to it.AlertNotificationandSummaryNotificationare refined abstractions — they add formatting or urgency logic on top of the base behavior.MessageSenderis the implementor interface. It declares only the primitivedeliveroperation.EmailSender,SmsSender, andSlackSenderare concrete implementors. They know the delivery mechanics but nothing about notification types.
Implementation
The implementations below model the notification system described above. All four versions demonstrate the same behavior: an AlertNotification can be paired with any sender at construction time, and SummaryNotification can be paired with a different one — no subclasses needed for the combinations.
// Implementor interface
interface MessageSender {
deliver(to: string, body: string): void;
}
// Concrete implementors
class EmailSender implements MessageSender {
deliver(to: string, body: string): void {
console.log(`[Email] To: ${to} | ${body}`);
}
}
class SmsSender implements MessageSender {
deliver(to: string, body: string): void {
console.log(`[SMS] To: ${to} | ${body}`);
}
}
class SlackSender implements MessageSender {
deliver(to: string, body: string): void {
console.log(`[Slack] Channel: ${to} | ${body}`);
}
}
// Abstraction
abstract class Notification {
constructor(protected sender: MessageSender) {}
abstract send(recipient: string, message: string): void;
}
// Refined abstractions
class AlertNotification extends Notification {
send(recipient: string, message: string): void {
this.sender.deliver(recipient, `ALERT: ${message.toUpperCase()}`);
}
}
class SummaryNotification extends Notification {
send(recipient: string, message: string): void {
this.sender.deliver(recipient, `Summary: ${message}`);
}
}
// Usage — mix and match without new subclasses
const emailAlert = new AlertNotification(new EmailSender());
const slackSummary = new SummaryNotification(new SlackSender());
const smsAlert = new AlertNotification(new SmsSender());
emailAlert.send("ops@example.com", "disk usage above 90%");
// [Email] To: ops@example.com | ALERT: DISK USAGE ABOVE 90%
slackSummary.send("#reports", "3 new sign-ups today");
// [Slack] Channel: #reports | Summary: 3 new sign-ups today
smsAlert.send("+15550001234", "payment failed");
// [SMS] To: +15550001234 | ALERT: PAYMENT FAILEDComposition over Inheritance
Bridge is one of the clearest illustrations of why composition beats deep inheritance for extensibility. Instead of encoding every combination into a class, you compose behavior at construction time. The sender field is the bridge — a single reference that connects two otherwise independent hierarchies. Changing an implementation at runtime is as simple as swapping the reference.
Real-World Examples
Cross-platform UI rendering — GUI frameworks like Qt and Java's AWT separate the logical widget hierarchy (Button, TextInput, Checkbox) from the platform rendering backend (Windows GDI, macOS CoreGraphics, Linux X11). Adding a new widget type does not require new renderers; adding a new platform does not require new widget types.
Database drivers — ORMs such as SQLAlchemy (Python) and GORM (Go) define a high-level query abstraction (session, query builder, model) that bridges to a concrete dialect/driver (PostgreSQL, MySQL, SQLite). Application code stays the same regardless of which database is underneath.
Logging frameworks — SLF4J in the Java ecosystem is a textbook Bridge. Application code calls the SLF4J Logger interface (the abstraction). At runtime you bind a concrete backend — Logback, Log4j 2, java.util.logging — without changing a single log call in business code. The binding is the bridge.
Remote rendering engines — game engines that support multiple graphics APIs (DirectX, Vulkan, Metal, OpenGL) use a Bridge to separate scene-graph objects (Mesh, Light, Camera) from the GPU-specific render commands. Switching render backends is a configuration change, not a refactor.
Pros and Cons
Advantages
Independent extensibility — you can add new abstractions and new implementations without modifying existing code on either side, satisfying the Open/Closed Principle.
Avoids class explosion — m abstractions and n implementations require m + n classes instead of m × n subclasses.
Runtime flexibility — the implementation can be swapped at construction time (or even at runtime) because it is held by reference, not baked into the type.
Cleaner separation of concerns — high-level logic lives in the abstraction hierarchy; low-level mechanics live in the implementor hierarchy. Each side is easier to understand and test independently.
Encapsulation of implementation details — clients program to the abstraction and never see concrete implementor classes, reducing coupling.
Disadvantages
Added indirection — for simple problems with only one or two implementations, Bridge introduces an extra layer of abstraction that makes the code harder to follow without clear benefit.
Upfront design cost — identifying the right split between abstraction and implementation requires foresight. The wrong split leads to an awkward design that is harder to change than a plain hierarchy would have been.
More initial files — the pattern always involves at least four classes (abstraction, refined abstraction, implementor interface, concrete implementor), which can feel heavyweight for small systems.
Interface stability matters — the implementor interface is a contract both sides depend on. Changing it (adding a method, changing a signature) forces updates to every concrete implementor.
When to Use / When to Avoid
Use Bridge when:
- You anticipate growth along two independent dimensions (e.g., notification types and delivery channels).
- You want to switch implementations at runtime, or make the implementation selectable through configuration.
- You are working with platform-specific code and want the rest of the application to remain platform-agnostic.
- You need to share an implementation object among multiple abstractions without tight coupling.
- A growing inheritance hierarchy is starting to feel unmanageable.
Avoid Bridge when:
- The abstraction and implementation are unlikely to vary independently — if they always change together, the bridge adds complexity without payoff.
- You have only one implementation now and no credible reason to expect a second. Apply YAGNI; add the bridge when the second implementation actually arrives.
- The system is small and the indirection obscures rather than clarifies intent.
- You are building a library API where the extra interface layer would confuse consumers who just want a straightforward class to instantiate.
Related Patterns
Adapter — Adapter makes two existing, incompatible interfaces work together after the fact. Bridge is designed upfront to keep two hierarchies independent. Both use composition, but Adapter is a retrofit and Bridge is intentional architecture.
Abstract Factory — Abstract Factory can create the concrete implementor objects that Bridge needs. They pair well: Abstract Factory handles "which family of implementors to use" while Bridge handles "how abstractions use those implementors."
Strategy — Strategy swaps an algorithm inside a single object. Bridge separates entire parallel hierarchies. The structural similarity (both hold a reference to an interchangeable object) leads to frequent confusion, but the intent differs: Strategy is about behavior variation; Bridge is about decoupling two independently evolving hierarchies.
Decorator — Decorator adds responsibilities to a single abstraction by wrapping it. Bridge connects an abstraction to a separate implementation hierarchy. Decorators stack vertically; Bridge splits horizontally.
Design Tip
A useful heuristic for knowing when Bridge applies: draw your class hierarchy and count dimensions of variation. If you find yourself describing subclasses with compound adjectives — "SlackAlertNotification", "WindowsVectorRenderer", "MySQLReadOnlyRepository" — each adjective is likely a separate dimension. Extract the second dimension into its own hierarchy and connect them with Bridge.