Home › Low-Level Design › Bridge Pattern
B

Bridge Pattern

Split one thing that changes along two independent axes into two hierarchies that evolve on their own.

Low-Level DesignMedium9 min read

I reach for Bridge when a class is about to explode into a combinatorial mess because it tries to be two things at once. It looks abstract until you hit the exact pain it was built for, and then it clicks.

If you remember five things

Everything below is the defense of these five calls.

  1. The intent. Decouple an abstraction from its implementation so the two can vary independently, meeting through a reference instead of inheritance.
  2. The trigger. Two dimensions each varying on their own, with a class name that is two nouns glued together (like SmsAlert) and a third noun coming on each side.
  3. The payoff. Growth goes from multiplicative (M x C classes) to additive (M + C), and you pick the pairing at runtime, not at compile time.
  4. The structure. Abstraction and Refined Abstraction on one side, Implementor and Concrete Implementor on the other, joined by one aggregation arrow. That arrow is the whole pattern.
  5. The classic gotcha. It is code-identical to Strategy. The difference is intent: Bridge exists so both sides grow into full hierarchies.

This is an LLD design pattern. My goal is simple: by the end you should be able to recognize when Bridge fits, draw it, and write it under interview pressure in about 10 minutes.

1The problem it solves#

Picture a notification system. You start with two message types, Alert and Reminder, and you need to send each over Email and SMS. Easy enough, you make four classes:

  • EmailAlert
  • SmsAlert
  • EmailReminder
  • SmsReminder

Then product keeps asking.

  • Add Push. Add Slack. Add a new message type, Receipt.
  • Now you need every message type times every channel.
  • Three message types and four channels is 12 classes. Add one more of either and you are at 15 or 16.
  • The math is multiplicative: M message types times C channels equals M x C classes. Every new row forces C new classes, every new column forces M.

The root cause: you have two independent reasons to change.

  • The kind of message changes for one set of reasons: new types, new formatting rules, new copy.
  • The delivery channel changes for a completely different set: new transport, new vendor SDK, a provider swap from Twilio to a cheaper SMS gateway.
  • That is two different teams and two different release cadences.
  • Bind them in one inheritance tree and every new value on one axis forces you to re-cross the other. One class has two reasons to change, a direct Single Responsibility Principle violation.

Bridge breaks that coupling.

  • One axis becomes a hierarchy of abstractions.
  • The other becomes a hierarchy of implementations.
  • They meet through a reference, not through inheritance.
  • This is "prefer composition over inheritance" made concrete.
The one-sentence intent

Decouple an abstraction from its implementation so the two can vary independently. If you can only say one thing about Bridge, say that. Everything else in this guide is how and why.

2The idea (mental model)#

Here is how I describe it at the whiteboard. Two things both want to vary. Instead of one tree with the Cartesian product baked in, you build two separate trees and connect them with a single arrow.

The two sides.

  • The abstraction side is what the client talks to and carries the higher-level meaning. In our example that is the message: Alert, Reminder, Receipt.
  • The implementation side is the low-level mechanism: EmailSender, SmsSender, PushSender.
  • The abstraction holds a reference to an implementation and delegates the gritty work to it.

The win is additive growth instead of multiplicative.

  • New message type? Add one class on the abstraction side.
  • New channel? Add one class on the implementation side.
  • Three messages plus four channels is 7 classes, not 12.
  • At 10 message types and 10 channels, Bridge is 20 classes while inheritance is 100. The gap widens fast.
  • And you pick the pairing at runtime, which plain subclassing never allows: new Alert(new SmsSender()) versus new Alert(new PushSender()) is a decision you make live, not at compile time.
Red flag that fails you

Calling the second hierarchy "implementation" and then treating it like a Java interface implementation. "Implementation" here means the second tree that does the concrete low-level work, nothing more. If you get tangled in the name, rename them in your head to the real domain words (sender, renderer, driver) so you do not confuse yourself mid-interview.

3Structure#

Four roles:

  • Abstraction: the top-level type the client uses. It keeps a reference to an Implementor and defines high-level operations in terms of it.
  • Refined Abstraction: subclasses of the Abstraction. They add or specialize behavior and still delegate the primitive work downward.
  • Implementor: the interface for the second hierarchy. It exposes primitive operations, not the high-level ones.
  • Concrete Implementor: the actual mechanisms that do the work.

The bridge itself is the reference from Abstraction to Implementor. That single line is the whole pattern.

classDiagram class Message { #MessageSender sender +send(text) void } class Alert { +send(text) void } class Reminder { +send(text) void } class Receipt { +send(text) void } class MessageSender { +deliver(payload) void } class EmailSender { +deliver(payload) void } class SmsSender { +deliver(payload) void } class PushSender { +deliver(payload) void } Message <|-- Alert Message <|-- Reminder Message <|-- Receipt MessageSender <|-- EmailSender MessageSender <|-- SmsSender MessageSender <|-- PushSender Message o-- MessageSender
Two trees, not one. Left is messages, right is senders. The o-- from Message to MessageSender is the bridge. Notice messages do not inherit from senders, they hold one. That aggregation arrow is the single most important line on the board, so draw it last and point at it when you explain the pattern.
Pause before you read on: in the diagram above, how many classes would you add to support a brand-new Slack channel plus a brand-new Promo message type?
Reveal the answer
Two classes total. One `SlackSender` on the implementor side and one `PromoMessage` on the abstraction side, with zero edits to any existing class. In the old inheritance approach you would need one class per existing message type for Slack and one class per existing channel for Promo. That contrast, additive versus multiplicative, is the whole reason Bridge exists.

4Implementation in Java#

The implementor side first. These are the low-level transports.

  • The interface exposes exactly one primitive, deliver.
  • It knows nothing about alerts or receipts. That ignorance is the point.
interface MessageSender {
    void deliver(String payload);
}

class EmailSender implements MessageSender {
    public void deliver(String payload) {
        System.out.println("EMAIL: " + payload);
    }
}

class SmsSender implements MessageSender {
    public void deliver(String payload) {
        System.out.println("SMS: " + payload);
    }
}

class PushSender implements MessageSender {
    public void deliver(String payload) {
        System.out.println("PUSH: " + payload);
    }
}

Now the abstraction side.

  • The base class holds the bridge reference, marked final so no subclass can swap it mid-flight.
  • Subclasses shape the high-level behavior and lean on the sender for the actual delivery.
abstract class Message {
    protected final MessageSender sender;

    protected Message(MessageSender sender) {
        this.sender = sender;
    }

    public abstract void send(String text);
}

class Alert extends Message {
    public Alert(MessageSender sender) {
        super(sender);
    }

    public void send(String text) {
        sender.deliver("[URGENT] " + text);
    }
}

class Reminder extends Message {
    public Reminder(MessageSender sender) {
        super(sender);
    }

    public void send(String text) {
        sender.deliver("Reminder: " + text);
    }
}

class Receipt extends Message {
    public Receipt(MessageSender sender) {
        super(sender);
    }

    public void send(String text) {
        sender.deliver("Your receipt - " + text);
    }
}

Wiring it up. Notice the pairing is chosen at runtime, and the same Alert class works with any sender.

public class Demo {
    public static void main(String[] args) {
        Message urgentSms = new Alert(new SmsSender());
        urgentSms.send("Server down");

        Message receiptByEmail = new Receipt(new EmailSender());
        receiptByEmail.send("Order #5512 - $40");

        // swap the transport without touching the message class
        Message alertByPush = new Alert(new PushSender());
        alertByPush.send("Login from new device");
    }
}

The payoff, stated plainly.

  • Three message classes, three sender classes, every combination available.
  • Adding SlackSender is one new class and zero changes to any Message.
  • Adding PromoMessage is one new class and zero changes to any sender.
  • That is a clean Open/Closed Principle win: open for extension on either axis, closed for modification on the other.

Here is the runtime flow for a single send:

sequenceDiagram participant Client participant Alert participant SmsSender Client->>Alert: send("Server down") Alert->>Alert: format "[URGENT] Server down" Alert->>SmsSender: deliver(payload) SmsSender-->>Client: sent
Delegation in one call. The abstraction does the high-level framing (the [URGENT] prefix), then hands the primitive work to whatever implementor it was given. The client never sees SmsSender, it only knows it holds a Message.

5A real-world example#

Device and driver designs are the clearest case. Think about a media playback library.

  • One axis is logical players: AudioPlayer, VideoPlayer, PodcastPlayer, each with its own controls and metadata handling.
  • The other axis is rendering backends: a software decoder, a GPU-accelerated decoder, a remote-cast target.
  • You do not want GpuVideoPlayer, SoftwareVideoPlayer, CastVideoPlayer, and the same triple for audio and podcasts. That is 9 classes for a 3-by-3 grid.
  • Player is the abstraction, backend is the implementor, and the bridge plays any content type on any backend with 6 classes.

It also shows up in cross-platform UI toolkits.

  • A Button or Window abstraction holds a reference to a platform-specific peer: WinApiPeer, CocoaPeer, X11Peer.
  • The widget hierarchy grows with new widget types, the peer hierarchy grows with new platforms, and neither forces churn in the other.
  • This is exactly how the original AWT peer model was structured.

JDBC is the example interviewers love.

  • Your code talks to the JDBC API, the abstraction.
  • The actual driver (MySQL, Postgres, Oracle) is the implementor, swapped in at runtime via the driver JAR on the classpath.
  • You change the driver without rewriting your queries.
  • Same shape: one stable abstraction, many interchangeable implementors.

6When to use it, and when not to#

Reach for Bridge
Two axes that each vary
Signal
You can feel the class count about to multiply.
Runtime
You want to switch the implementation live, not just at compile time.
Isolation
You want a messy or third-party implementation hidden behind a stable abstraction, and you expect it to change.
Skip Bridge
One axis, or locked axes
One dimension
Plain inheritance or Strategy is enough. Bridge is just extra indirection.
Locked together
If the two dimensions will never recombine, a split buys nothing and costs readability.
Too early
One message type and one sender? Wait for the second axis to appear. It is cheap to refactor in later.

My rule of thumb: Bridge earns its place the moment you catch yourself writing a class name that is two nouns glued together, like SmsAlert, and you can see a third noun coming on each side.

7Common mistakes#

Confusing it with Strategy.

  • They look identical in code: an object holding a reference to an interface.
  • The difference is intent. Strategy swaps one algorithm behind a stable context. Bridge is structural, it exists so both sides can grow into full hierarchies.
  • If only one side ever has subclasses, you have Strategy, and that is fine, just call it what it is.
  • The giveaway: in Bridge, both the abstraction and the implementor have multiple concrete types.

Putting high-level operations in the Implementor.

  • The implementor should expose primitives. deliver(payload) is primitive.
  • Put sendUrgentAlert() on MessageSender and the abstraction loses its reason to exist, collapsing back into one coupled tree.
  • Keep the implementor dumb and low-level.

Leaking implementor types to the client.

  • If callers have to instanceof the sender or cast it, the bridge is broken.
  • The client should know only the abstraction and pick an implementor at construction time.

Building it too early.

  • The most common one in real code.
  • Someone reads about Bridge and applies it to a single-axis problem, and now there is a two-tree maze for no benefit.
  • Wait for the second dimension to be real.

Making the abstraction know too much about a specific implementor.

  • The base Message talks to MessageSender, never to SmsSender.
  • The minute concrete-to-concrete knowledge creeps in, you have re-coupled the trees.
  • This is the Dependency Inversion Principle: both sides depend on the MessageSender abstraction, not on each other's concretions.

If only one side ever grows a hierarchy, you built Strategy and gave it the wrong name.

8Where it shows up in interviews#

Interviewers rarely say "use Bridge." They hand you a prompt that smells like it. Watch for these:

  • "Design a notification service that supports multiple message types across multiple channels." The textbook setup.
  • "Design a remote control that works with different devices like TV, radio, speaker." Remote types on one axis, devices on the other.
  • "Design a shape drawing system where shapes render on different platforms or output formats." Shapes versus renderers.
  • "Design a payment system with different payment methods across different gateways." Method on one axis, gateway on the other.

The signal to name out loud:

There are two things varying independently here, so I will split them into an abstraction hierarchy and an implementation hierarchy and bridge them with a reference, which keeps growth additive instead of multiplicative.

Saying that sentence shows you understand the why, not just the diagram.

Follow-ups the interviewer will ask (and your answers)
"How is this different from Strategy? They look the same."
In code they are nearly identical: an object holding a reference to an interface. The difference is intent. Strategy swaps one algorithm behind a stable context, so only the algorithm side has variants. Bridge is structural: both the abstraction and the implementor grow into full hierarchies. If only one side ever gets subclasses, I call it Strategy, not Bridge.
"Now add a Slack channel."
One new class, `SlackSender implements MessageSender`, and nothing else changes. Every existing message type works with it immediately because they only ever talk to the `MessageSender` interface. That additive growth is the whole point of the pattern.
"Now add a PromoMessage type."
Again one class, `PromoMessage extends Message`, with zero edits to any sender. The symmetry is the tell: a new value on either axis costs exactly one class. That is Bridge turning M x C into M + C.
"Why not just put sendUrgentAlert() on the sender interface?"
Because that drags high-level meaning into the implementor, and the abstraction loses its reason to exist. The implementor must expose only primitives like `deliver(payload)`. The moment the sender knows what an "urgent alert" is, the two trees re-couple and you are back to one inheritance mess.
"When would you not use Bridge here?"
If there is genuinely one axis. One message type and one sender, or two dimensions that will never recombine, means Bridge is just indirection for its own sake. I would start with plain inheritance or Strategy and refactor into Bridge when the second axis actually shows up, which is cheap to do later.

What you can now explain in the room

  • The one-sentence intent: decouple an abstraction from its implementation so both vary independently.
  • Why gluing two varying axes into one inheritance tree causes multiplicative class blowup and a Single Responsibility violation.
  • How Bridge turns M x C classes into M + C, and lets you pick the pairing at runtime.
  • The four roles and why the aggregation arrow, not inheritance, is the whole pattern.
  • How to write it in Java: a `final` implementor reference in the abstraction, primitives only in the implementor.
  • The classic gotcha: Bridge and Strategy are code-identical, and the difference is whether both sides grow hierarchies.

If this clicked, look at Strategy next (its close cousin), then Adapter and Abstract Factory, which pair naturally with Bridge in larger designs.

Get a prep plan built around your Bridge Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Bridge Pattern prep plan is on the way.