Bridge Pattern: Decoupling Abstraction from Implementation
How to let an abstraction and its implementation vary independently, using composition instead of a combinatorial inheritance hierarchy.
Bridge Pattern
Intent: decouple an abstraction from its implementation so the two can vary independently. Instead of hard-wiring "what" (the abstraction) to "how" (the implementation) through inheritance, Bridge connects them through composition — a reference held by the abstraction to an implementation interface.
Bridge is the pattern for when you notice your class hierarchy has two independent dimensions of variation trying to live in one inheritance tree, and every new value along either dimension multiplies the number of classes you need.
Bridge is named for what it builds: a bridge between two hierarchies that would otherwise have to merge into one combinatorial mess. Picture "remote controls" (abstraction: basic remote, advanced remote) and "devices" (implementation: TV, radio, projector) — if every remote type needs a version for every device type, you get BasicTvRemote, BasicRadioRemote, AdvancedTvRemote, AdvancedRadioRemote... Bridge lets any remote work with any device through one connecting interface.
1. The problem: two dimensions of variation, one inheritance tree
Say you're building a shape-drawing library. Shapes (Circle, Square) need to render through different drawing APIs (a fast in-memory RasterAPI, a VectorAPI for print/export). Both dimensions are expected to grow: more shapes, more rendering backends.
VIOLATION: inheritance tries to encode both dimensions at once
// VIOLATION: one subclass per (shape x rendering API) combination
abstract class Shape {
abstract void draw();
}
class CircleRaster extends Shape {
void draw() { /* draw circle using raster pixels */ }
}
class CircleVector extends Shape {
void draw() { /* draw circle using vector paths */ }
}
class SquareRaster extends Shape {
void draw() { /* draw square using raster pixels */ }
}
class SquareVector extends Shape {
void draw() { /* draw square using vector paths */ }
}
// Adding Triangle means 2 new classes. Adding a third rendering API (SVG)
// means 3 new classes for the shapes that already exist, PLUS every future
// shape needs a 3rd variant too. Shapes x RenderingAPIs classes, forever.Every new shape requires knowing about every rendering API, and every new rendering API requires touching every shape. The two concerns — what shape is this and how does it get rendered — are supposed to be independent, but inheritance has welded them together.
2. The fix: split into two hierarchies connected by composition
// Implementation hierarchy: HOW a shape gets rendered — completely shape-agnostic
interface DrawingAPI {
void drawCircle(double x, double y, double radius);
void drawSquare(double x, double y, double side);
}
class RasterAPI implements DrawingAPI {
public void drawCircle(double x, double y, double radius) { /* rasterize pixels */ }
public void drawSquare(double x, double y, double side) { /* rasterize pixels */ }
}
class VectorAPI implements DrawingAPI {
public void drawCircle(double x, double y, double radius) { /* emit vector path */ }
public void drawSquare(double x, double y, double side) { /* emit vector path */ }
}
// Abstraction hierarchy: WHAT the shape is — holds a reference to the implementation (the "bridge")
abstract class Shape {
protected final DrawingAPI drawingApi; // the bridge
Shape(DrawingAPI drawingApi) {
this.drawingApi = drawingApi;
}
abstract void draw();
}
class Circle extends Shape {
private final double x, y, radius;
Circle(double x, double y, double radius, DrawingAPI drawingApi) {
super(drawingApi);
this.x = x; this.y = y; this.radius = radius;
}
void draw() {
drawingApi.drawCircle(x, y, radius); // delegates the "how" across the bridge
}
}
class Square extends Shape {
private final double x, y, side;
Square(double x, double y, double side, DrawingAPI drawingApi) {
super(drawingApi);
this.x = x; this.y = y; this.side = side;
}
void draw() {
drawingApi.drawSquare(x, y, side);
}
}// Any shape works with any rendering API — mix and match freely, at runtime
Shape circleOnScreen = new Circle(0, 0, 5, new RasterAPI());
Shape circleForPrint = new Circle(0, 0, 5, new VectorAPI());
Shape squareOnScreen = new Square(1, 1, 3, new RasterAPI());
circleOnScreen.draw();
circleForPrint.draw();Adding Triangle now means one new Shape subclass — it automatically works with RasterAPI and VectorAPI both, and with any future DrawingAPI implementation. Adding an SvgAPI means one new DrawingAPI implementation — it automatically works with Circle, Square, and any future Shape subclass. The two hierarchies genuinely stopped multiplying each other.
Bridge is fundamentally "prefer composition over inheritance" applied at the design stage, before the combinatorial hierarchy is even built — rather than as a refactor after the fact. If you can identify two axes of variation before writing the first class, you can go straight to Bridge and skip the subclass explosion entirely.
3. Bridge vs. Adapter
Both patterns involve one object holding a reference to another and delegating through an interface — the structure looks similar. The difference is when and why the split happens:
| Bridge | Adapter | |
|---|---|---|
| When designed | Up front, at design time — you deliberately split abstraction from implementation before either grows | After the fact — you're stuck making a pre-existing incompatible interface work with a pre-existing expected one |
| Purpose | Let two hierarchies evolve independently | Make two interfaces that don't match, match |
| Who controls both sides | You — you design both the abstraction and the implementation interface together | Usually not — the adaptee is often a third-party or legacy class you don't control |
| Number of implementations expected | Typically several on each side, growing over time (that's the point) | Typically one adaptee per adapter — you're bridging a specific incompatibility, not designing for growth |
See Adapter Pattern for the Adapter side of this in full. A useful gut check: if you're asking "how do I make my two hand-picked interfaces cooperate as they both grow," that's Bridge. If you're asking "how do I make this one interface I don't control match this other one I do," that's Adapter.
4. Bridge vs. Strategy
This is the pairing that trips people up most in interviews, because both patterns look identical in UML — a class holding a reference to an interface, delegating a call to it.
| Bridge | Strategy | |
|---|---|---|
| Dimensions of variation | Two, both expected to have multiple subclasses growing over time (Shape subtypes AND DrawingAPI subtypes) | One — the algorithm. The context class itself usually isn't part of a parallel hierarchy |
| Intent | Structural — decouple two class hierarchies so each can be extended independently | Behavioral — make one algorithm swappable at runtime |
| Is the "held" object usually swapped at runtime? | Sometimes, but the real point is the two hierarchies not needing to know about each other's growth | Yes — that's the whole point, swap the algorithm per call or per context |
| Motivating question | "I have two things that both vary — how do I stop them multiplying each other?" | "I have one behavior that needs multiple interchangeable implementations" |
In practice, many textbook "Strategy" examples (DiscountCalculator holding a DiscountStrategy) are structurally indistinguishable from a minimal Bridge with only one abstraction subclass. The distinguishing question isn't the code shape — it's intent: was this designed to let two hierarchies grow independently (Bridge), or to make one algorithm pluggable (Strategy)? If in doubt in an interview, state the ambiguity and justify your pick from the problem's growth dimensions.
5. When to use vs. when it's overkill
| Use Bridge when | Skip it when |
|---|---|
| You can identify two genuinely independent dimensions of variation, both expected to grow | There's only one dimension of variation — a plain interface with a few implementations is enough |
Subclass count is growing multiplicatively (ShapeXRenderer style naming is a strong signal) | The "second dimension" is actually fixed and unlikely to ever add a new value |
| You want to swap an implementation at runtime without touching the abstraction | Both sides are simple enough that a single small class hierarchy is genuinely clearer |
| Implementation details (e.g., platform-specific APIs) shouldn't leak into the abstraction's public interface | You're only trying to fix a one-off interface mismatch — that's Adapter, not Bridge |
6. Real-world bridges
- JDBC:
java.sql.Driver/Connectionis the implementation hierarchy (MySQL driver, PostgreSQL driver, ...); your application's use ofjava.sql.*interfaces is the abstraction side — you write againstConnection/Statementand the actual vendor implementation varies independently underneath. - SLF4J: the
Loggerabstraction is bridged to whatever concrete logging implementation (Logback, Log4j2, JUL) is on the classpath at runtime — new logging backends don't require changes to code that logs against theLoggerinterface, and vice versa. - AWT/Swing's peer architecture: UI component classes (
Button,Checkbox) are bridged to platform-specific peer implementations (ButtonPeer) that vary by operating system, so the component hierarchy doesn't multiply per OS. - Cross-platform mobile abstractions: a shared business-logic layer bridged to platform-specific implementations (iOS vs Android storage, notifications, biometric auth) behind one interface per capability.
Interview Questions
- What two "dimensions of variation" does the shape/drawing-API example have, and what would happen to the class count if you added a third shape and a third rendering API without Bridge?
- Explain how Bridge is "prefer composition over inheritance," applied proactively at design time rather than as a later refactor.
- Compare Bridge and Adapter. Both hold a reference to another interface and delegate — what's the actual distinguishing question?
- Compare Bridge and Strategy. Why do they often look identical in a UML diagram, and what tells them apart?
- Give a real standard-library or framework example of Bridge, and identify which side is the abstraction and which is the implementation.
- When would introducing a Bridge be premature abstraction? What has to be true about your problem for Bridge to pay off?
- If you only ever have one concrete implementation of
DrawingAPIand it's unlikely to ever have a second, does Bridge still make sense? Why or why not?