03-structural-patterns

Bridge Pattern: Decoupling Abstraction from Implementation

How to let an abstraction and its implementation vary independently, using composition instead of a combinatorial inheritance hierarchy.

August 11, 2026
lldstructuralbridgecompositionabstraction

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

java
// 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

java
// 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);
    }
}
java
// 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:

BridgeAdapter
When designedUp front, at design time — you deliberately split abstraction from implementation before either growsAfter the fact — you're stuck making a pre-existing incompatible interface work with a pre-existing expected one
PurposeLet two hierarchies evolve independentlyMake two interfaces that don't match, match
Who controls both sidesYou — you design both the abstraction and the implementation interface togetherUsually not — the adaptee is often a third-party or legacy class you don't control
Number of implementations expectedTypically 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.

BridgeStrategy
Dimensions of variationTwo, 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
IntentStructural — decouple two class hierarchies so each can be extended independentlyBehavioral — 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 growthYes — 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 whenSkip it when
You can identify two genuinely independent dimensions of variation, both expected to growThere'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 abstractionBoth 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 interfaceYou're only trying to fix a one-off interface mismatch — that's Adapter, not Bridge

6. Real-world bridges

  • JDBC: java.sql.Driver/Connection is the implementation hierarchy (MySQL driver, PostgreSQL driver, ...); your application's use of java.sql.* interfaces is the abstraction side — you write against Connection/Statement and the actual vendor implementation varies independently underneath.
  • SLF4J: the Logger abstraction 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 the Logger interface, 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 DrawingAPI and it's unlikely to ever have a second, does Bridge still make sense? Why or why not?