Bridge Pattern
Bridge splits one class hierarchy into two independent axes: abstraction (what the client uses) and implementation (how work is done).
Introduction
Bridge splits one class hierarchy into two independent axes: abstraction (what the client uses) and implementation (how work is done). The abstraction holds an implementation reference and delegates. Shape × Renderer, Notification × Channel, Report × OutputFormat — vary both axes without N×M subclass explosion.
The story
A monitoring product had EmailNotification, SmsNotification, PushNotification, each in three urgency levels — nine subclasses. Adding Slack would add three more. Splitting Notification (urgency) from Channel (delivery) replaced nine classes with combinations via Bridge.
The business problem
Single inheritance mixing two orthogonal axes causes cartesian explosion:
- N×M subclasses: 30 report types × 3 output formats = 90 classes.
- Release coupling: new channel forces new subclass of every urgency level.
- Runtime selection: implementation chosen per platform/environment at runtime.
- Test matrix blowup: every abstraction × every implementation combination.
The problem teams faced
Bridge fits when:
- Two independent dimensions vary (message × channel, shape × renderer).
- Both axes evolve at different rates.
- Implementation selected at runtime (platform, config, tenant).
- Inheritance hierarchy would produce N×M concrete classes.
Understanding the topic
Intent: Decouple abstraction from implementation so both vary independently.
- Abstraction — high-level interface; holds Implementor reference.
- RefinedAbstraction — subclasses adding higher-level behaviour.
- Implementor — interface for low-level implementation hierarchy.
- ConcreteImplementor — platform/vendor specific implementations.
Internal architecture
Notification × Channel bridge:
interface Channel { send(message: string, recipient: string): Promise<void> }class EmailChannel implements Channel { /* ... */ }class SlackChannel implements Channel { /* ... */ }abstract class Notification {constructor(protected channel: Channel) {}abstract dispatch(recipient: string, body: string): Promise<void>}class UrgentNotification extends Notification {async dispatch(recipient: string, body: string) {await this.channel.send(`[URGENT] ${body}`, recipient)}}
Visual explanation
Three diagrams cover two axes, vs Adapter, and injection:
Informative example
Before / after — N×M subclasses to Bridge:
// ❌ Cartesian explosion — 9 notification classesclass UrgentEmailNotification { send() { /* ... */ } }class UrgentSmsNotification { send() { /* ... */ } }// ... 7 more; adding Slack adds 3 more// ✅ Bridge — urgency × channel independentinterface Channel { deliver(msg: string, to: string): Promise<void> }abstract class Notification {constructor(protected channel: Channel) {}abstract format(body: string): stringasync send(to: string, body: string) {await this.channel.deliver(this.format(body), to)}}class UrgentNotification extends Notification {format(body: string) { return `[URGENT] ${body}` }}// Wire at composition rootconst notif = new UrgentNotification(new SlackChannel(slackConfig))
Execution workflow
Identify two axes
Orthogonal variation — shape×renderer, report×format.
Real-world use
JDBC (Connection API bridges to vendor drivers), SLF4J (logging API + bindings), JDBC drivers, cross-platform GUI peers, Spring Cloud Stream binders, driver abstractions in graphics engines.
Production case study
Reporting platform output formats:
- Scenario: 30 report types × PDF, HTML, XLSX outputs.
- Naive: 90 classes.
- Bridge: 30 Report abstractions × 3 OutputFormat implementors = 33 classes.
- Outcome: CSV output = 1 new implementor, not 30 new classes.
Trade-offs
- Pro: independent variation on two axes; runtime implementor swap.
- Pro: smaller class count — N+M not N×M.
- Con: indirection — two hierarchies to navigate.
- Con: overkill when only one axis actually varies.
Decision framework
- Use when two orthogonal dimensions both grow in git history.
- Use when implementor chosen at runtime per platform/tenant.
- Avoid when single axis varies — Strategy or Factory suffices.
- Avoid retrofitting Bridge when Adapter solves integration only.
Best practices
- Name axes clearly — Abstraction = domain concept, Implementor = platform/mechanism.
- Inject Implementor at composition root — not new inside abstraction.
- JDBC mental model: Connection is abstraction, Driver is implementor.
Anti-patterns to avoid
- Bridge when only one axis ever varied — premature split.
- Implementor leaking into abstraction public API.
- Confusing Bridge with Adapter — different intent.
Common mistakes
- Creating Bridge before second axis exists — YAGNI.
- Abstraction calling wrong implementor for platform — wire carefully at root.
Debugging tips
- Log which Implementor injected at startup per deployment profile.
- Matrix test: sample each Abstraction × each Implementor.
Optimization strategies
- Pool or cache Implementor when construction expensive ( JDBC-style).
Common misconceptions
- Bridge ≠ Adapter. Bridge separates planned axes; Adapter translates legacy.
- Strategy can be Implementor side of a Bridge.
- Dependency injection of driver is Bridge in practice.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionBridge vs Adapter?+
Answer
Follow-up
2AdvancedQuestionBridge vs Strategy?+
Answer
Follow-up
3IntermediateQuestionJDBC as Bridge?+
Answer
Follow-up
4AdvancedQuestionWhen Bridge overkill?+
Answer
Follow-up
Summary
You can identify orthogonal axes, split abstraction from implementor, and avoid cartesian subclass explosion. Tell the nine-notification-class story — if you can do that, you own Bridge.