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.
- The intent. Decouple an abstraction from its implementation so the two can vary independently, meeting through a reference instead of inheritance.
- 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. - 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.
- 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.
- 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:
EmailAlertSmsAlertEmailReminderSmsReminder
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.
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())versusnew Alert(new PushSender())is a decision you make live, not at compile time.
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.
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.Reveal the answer
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
finalso 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
SlackSenderis one new class and zero changes to anyMessage. - Adding
PromoMessageis 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:
[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
ButtonorWindowabstraction 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#
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.
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()onMessageSenderand 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
instanceofthe 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
Messagetalks toMessageSender, never toSmsSender. - 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
MessageSenderabstraction, 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.
"How is this different from Strategy? They look the same."
"Now add a Slack channel."
"Now add a PromoMessage type."
"Why not just put sendUrgentAlert() on the sender interface?"
"When would you not use Bridge here?"
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.