Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 29

    Bridge Pattern

    Bridge splits one class hierarchy into two independent axes: abstraction (what the client uses) and implementation (how work is done).

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    ts
    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:

    Two independent axes
    Abstraction
    Notification · Report
    has-a
    Composition
    Implementor
    Channel · OutputFormat
    N + M classes
    Not N × M
    Bridge designed up-front for two varying dimensions.
    Bridge vs inheritance explosion
    3 channels × 3 levels
    9 subclasses
    Bridge split
    3 + 3 = 6
    Add Slack channel
    +1 implementor
    Not +3 subclasses
    Open/Closed
    New implementor, not new subclass of every abstraction.
    Bridge vs Adapter
    Designed two axes?
    Bridge
    Retrofit incompatible?
    Adapter
    Both use composition
    Yes
    Intent differs
    Variation vs integration
    Bridge is planned separation; Adapter is after-the-fact translation.

    Informative example

    Before / after — N×M subclasses to Bridge:

    ts
    // ❌ Cartesian explosion — 9 notification classes
    class UrgentEmailNotification { send() { /* ... */ } }
    class UrgentSmsNotification { send() { /* ... */ } }
    // ... 7 more; adding Slack adds 3 more
    // ✅ Bridge — urgency × channel independent
    interface Channel { deliver(msg: string, to: string): Promise<void> }
    abstract class Notification {
    constructor(protected channel: Channel) {}
    abstract format(body: string): string
    async 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 root
    const notif = new UrgentNotification(new SlackChannel(slackConfig))

    Execution workflow

    1Bridge design workflow
    1 / 4

    Identify two axes

    Orthogonal variation — shape×renderer, report×format.

    Draw 2×2 matrix; if both axes grow, Bridge fits.

    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.

    4 questions
    1IntermediateQuestionBridge vs Adapter?+

    Answer

    Bridge: designed up-front to split two varying axes. Adapter: retrofit to translate incompatible existing interface.

    Follow-up

    Same composition?
    2AdvancedQuestionBridge vs Strategy?+

    Answer

    Strategy varies one algorithm. Bridge separates entire implementation hierarchy from abstraction hierarchy — two dimensions.

    Follow-up

    Notification × Channel?
    3IntermediateQuestionJDBC as Bridge?+

    Answer

    Connection/Statement abstraction; vendor Driver is Implementor. Swap driver without changing app JDBC code.

    Follow-up

    Where inject driver?
    4AdvancedQuestionWhen Bridge overkill?+

    Answer

    When only one axis varies in production — e.g., only output format changes, report types fixed.

    Follow-up

    Evidence to wait for?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.