The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It uses composition instead of inheritance to split a monolithic class hierarchy into two orthogonal dimensions.

Intent and Motivation

Intent: Decouple an abstraction from its implementation so that the two can vary independently.

Motivation: Imagine a remote control (abstraction) and devices like TVs and radios (implementations). A naive design creates TVRemote, RadioRemote, AdvancedTVRemote, AdvancedRadioRemote — exponential subclass explosion. Bridge extracts the implementation into a separate hierarchy (Device interface with TV, Radio) and the abstraction holds a reference to a Device. Now you can combine any remote with any device: BasicRemote + SonyTV, AdvancedRemote + Radio.

This is critical when both the abstraction and implementation need to evolve independently — rendering engines, database drivers, messaging transports.

Structure (UML-like)

  ┌───────────────────┐         has-a        ┌───────────────────┐
│    Abstraction    │ ───────────────────► │  Implementor      │ (interface)
├───────────────────┤                       ├───────────────────┤
│ - implementor     │                       │ + operationImpl() │
│ + operation()     │                       └─────────▲─────────┘
└─────────▲─────────┘                                 │
          │ extends                                     │ implements
┌─────────┴─────────┐                          ┌────────┴────────┐
│RefinedAbstraction │                          │ConcreteImplementor│
└───────────────────┘                          └───────────────────┘
  

Participants:

  • Abstraction — defines the high-level interface, delegates to Implementor.
  • RefinedAbstraction — extended abstraction with additional behavior.
  • Implementor — interface for implementation classes.
  • ConcreteImplementor — platform-specific or variant-specific implementation.

Java Example

  // Implementor
interface Device {
    boolean isEnabled();
    void enable();
    void disable();
    int getVolume();
    void setVolume(int percent);
}

class TV implements Device {
    private boolean on = false;
    private int volume = 50;

    public boolean isEnabled() { return on; }
    public void enable() { on = true; System.out.println("TV on"); }
    public void disable() { on = false; System.out.println("TV off"); }
    public int getVolume() { return volume; }
    public void setVolume(int v) { volume = v; System.out.println("TV volume: " + v); }
}

// Abstraction
abstract class RemoteControl {
    protected Device device;

    RemoteControl(Device device) { this.device = device; }

    void togglePower() {
        if (device.isEnabled()) device.disable();
        else device.enable();
    }

    void volumeUp() { device.setVolume(device.getVolume() + 10); }
}

class BasicRemote extends RemoteControl {
    BasicRemote(Device device) { super(device); }
}

class AdvancedRemote extends RemoteControl {
    AdvancedRemote(Device device) { super(device); }

    void mute() { device.setVolume(0); System.out.println("Muted"); }
}

// Usage — any remote works with any device
RemoteControl remote = new AdvancedRemote(new TV());
remote.togglePower();
remote.volumeUp();
((AdvancedRemote) remote).mute();
  

JavaScript Example

  // Implementor — rendering backends
class CanvasRenderer {
  drawCircle(x, y, r) {
    console.log(`Canvas: circle at (${x},${y}) r=${r}`);
  }
}

class SVGRenderer {
  drawCircle(x, y, r) {
    console.log(`SVG: <circle cx="${x}" cy="${y}" r="${r}"/>`);
  }
}

// Abstraction
class Shape {
  constructor(renderer) {
    this.renderer = renderer;
  }
}

class Circle extends Shape {
  constructor(renderer, x, y, radius) {
    super(renderer);
    this.x = x; this.y = y; this.radius = radius;
  }

  draw() {
    this.renderer.drawCircle(this.x, this.y, this.radius);
  }
}

// Combine any shape with any renderer
new Circle(new CanvasRenderer(), 10, 20, 5).draw();
new Circle(new SVGRenderer(), 10, 20, 5).draw();
  

Real-World Use Cases

Framework / System Usage
JDBC Connection (abstraction) bridges to vendor-specific drivers (implementor).
SLF4J Logging facade bridges to Logback, Log4j, or JUL implementations.
AWT / Java2D Platform-independent graphics API bridges to OS-native rendering.
React Native JavaScript components bridge to native iOS/Android UI implementations.
AWS SDK v2 Service clients (abstraction) bridge to HTTP client implementations (Netty, Apache).
Database abstraction layers Sequelize, TypeORM — ORM API bridges to MySQL, PostgreSQL, SQLite drivers.

Pros and Cons

Pros Cons
Platform independence — swap implementations at runtime or compile time Increases complexity for simple, stable hierarchies
Hides implementation details from clients Requires careful design upfront to identify orthogonal dimensions
Both abstraction and implementation can be extended independently Indirection adds a layer that must be understood
Replaces fragile multi-dimensional inheritance with composition Over-abstraction when only one implementation will ever exist
Follows Single Responsibility and Open/Closed principles More classes and interfaces to maintain

When to Use vs When NOT to Use

Use when:

  • You want to avoid a permanent binding between abstraction and implementation.
  • Both abstraction and implementation need to be extended by subclassing.
  • Changes in implementation should not affect client code.
  • You need to share an implementation among multiple objects (reference counting, connection pooling).
  • You have a proliferation of classes from coupled abstraction-implementation inheritance.

Do NOT use when:

  • Only one implementation will ever exist and is unlikely to change.
  • The abstraction and implementation are tightly coupled by nature (no benefit from separation).
  • A simple inheritance hierarchy with 2–3 variants is sufficient.
  • You are retrofitting incompatible interfaces (use Adapter instead).

Common Mistakes

  1. Confusing Bridge with Adapter — Bridge is designed upfront; Adapter fixes existing incompatibility after the fact.
  2. Exposing the implementor to clients — clients should interact only with the abstraction.
  3. Too many abstraction levels — keep the hierarchy shallow; deep nesting obscures the pattern.
  4. Not injecting the implementor — hard-coding new ConcreteImplementor() inside the abstraction defeats the purpose.
  5. Using Bridge when Strategy suffices — if only the algorithm varies (not the entire implementation), Strategy is simpler.
  • Adapter — Bridge separates design-time concerns; Adapter makes existing classes work together.
  • Strategy — similar composition structure; Strategy swaps algorithms, Bridge swaps platform implementations.
  • Abstract Factory — can create and configure a Bridge’s implementor at creation time.
  • State — structure resembles Bridge; State changes behavior based on internal state, Bridge delegates to implementor.
  • Dependency Injection — modern DI containers wire Bridge abstractions to implementations via configuration.