Structural Pattern Pattern 7 of 23

Bridge

Some class hierarchies don't vary along one axis — they vary along two, independently, at the same time. The Bridge pattern answers the question: how do you let two independent dimensions of variation grow separately instead of forcing every combination of the two into its own subclass?

⚠️ The Problem

Imagine a drawing app with shapes — Circle and Square — that can each be drawn by different renderers — VectorRenderer and RasterRenderer. If you model this with plain inheritance, "shape type" and "renderer type" both live in the same hierarchy, so you end up needing a class for every combination: VectorCircle, RasterCircle, VectorSquare, RasterSquare. Add a third shape (Triangle) or a third renderer (PdfRenderer) and the number of classes doesn't grow by one or two — it multiplies. Two independent ideas ("what shape is it" and "how is it drawn") have been welded into one combinatorial hierarchy.

✅ The Solution

Split the two dimensions into two separate class hierarchies — a Shape hierarchy and a Renderer hierarchy — and connect them by composition instead of inheritance: each Shape holds a reference to a Renderer object and delegates the actual drawing work to it. Now a new shape needs exactly one new Shape subclass, and a new renderer needs exactly one new Renderer subclass — the two hierarchies vary independently, and any shape can be paired with any renderer at runtime.

Example 1 — shapes and renderers

Primary Example — JavaScript
// The "implementation" hierarchy: how to actually draw primitives.
class Renderer {
  renderCircle(radius) { throw new Error("not implemented"); }
  renderSquare(side) { throw new Error("not implemented"); }
}

class VectorRenderer extends Renderer {
  renderCircle(radius) {
    console.log(`Drawing a circle of radius ${radius} using vector paths`);
  }
  renderSquare(side) {
    console.log(`Drawing a square of side ${side} using vector paths`);
  }
}

class RasterRenderer extends Renderer {
  renderCircle(radius) {
    console.log(`Drawing a circle of radius ${radius} as a bitmap of pixels`);
  }
  renderSquare(side) {
    console.log(`Drawing a square of side ${side} as a bitmap of pixels`);
  }
}

// The "abstraction" hierarchy: what to draw. Holds a Renderer — the bridge.
class Shape {
  constructor(renderer) {
    this.renderer = renderer;
  }
}

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

class Square extends Shape {
  constructor(renderer, side) {
    super(renderer);
    this.side = side;
  }
  draw() {
    this.renderer.renderSquare(this.side);
  }
}

// Any shape + any renderer, mixed freely at runtime — no combinatorial explosion.
new Circle(new VectorRenderer(), 5).draw();
new Circle(new RasterRenderer(), 5).draw();
new Square(new VectorRenderer(), 3).draw();

Adding Triangle means writing one new Shape subclass. Adding PdfRenderer means writing one new Renderer subclass. Neither addition touches the other hierarchy, and every shape already works with every renderer automatically.

Example 2 — a remote control and the devices it controls

Secondary Example — C#
// The "implementation" hierarchy: devices that can be turned on/off and volume-adjusted.
public interface IDevice
{
    void TurnOn();
    void TurnOff();
    void SetVolume(int percent);
}

public class Television : IDevice
{
    public void TurnOn() => Console.WriteLine("TV: powering on");
    public void TurnOff() => Console.WriteLine("TV: powering off");
    public void SetVolume(int percent) => Console.WriteLine($"TV: volume set to {percent}%");
}

public class Radio : IDevice
{
    public void TurnOn() => Console.WriteLine("Radio: powering on");
    public void TurnOff() => Console.WriteLine("Radio: powering off");
    public void SetVolume(int percent) => Console.WriteLine($"Radio: volume set to {percent}%");
}

// The "abstraction" hierarchy: remote controls. Holds a device — the bridge.
public abstract class RemoteControl
{
    protected readonly IDevice Device;

    protected RemoteControl(IDevice device)
    {
        Device = device;
    }

    public virtual void TogglePower(bool on)
    {
        if (on) Device.TurnOn(); else Device.TurnOff();
    }
}

public class AdvancedRemoteControl : RemoteControl
{
    public AdvancedRemoteControl(IDevice device) : base(device) { }

    public void Mute() => Device.SetVolume(0);
}

// Any remote + any device, combined freely.
IDevice tv = new Television();
RemoteControl basicRemote = new AdvancedRemoteControl(tv);
basicRemote.TogglePower(true);

AdvancedRemoteControl advanced = new AdvancedRemoteControl(new Radio());
advanced.TogglePower(true);
advanced.Mute();

The remote-control hierarchy (basic vs. advanced) and the device hierarchy (TV vs. radio) each grow on their own — a new device type never requires touching the remote classes, and a new remote type never requires touching the device classes.

👍 When To Use It

  • You've identified two genuinely independent dimensions of variation in a design (e.g. "what" vs. "how"), and subclassing both together would multiply combinations.
  • You want to switch an object's implementation at runtime — e.g. swap a shape's renderer, or a remote's target device — without changing the abstraction's own code.
  • You want both hierarchies to be independently extensible by different teams or at different times, without either one needing to know about every member of the other.

👎 When NOT To Use It

  • When there's really only one dimension of variation — adding a bridge for a hierarchy that never needed to vary independently just adds an extra layer of indirection with no payoff.
  • When the two "hierarchies" are small and stable and unlikely to grow — a couple of straightforward subclasses may be simpler to read than an abstraction/implementation split.
  • Early in a design, before you're sure which axes actually vary independently — Bridge is easiest to justify once you've already felt the combinatorial pain, not before.

❓ FAQ

How is Bridge different from Adapter? They both wrap another object.

Adapter is applied after the fact, to make an existing, incompatible interface work with code that already expects something else — it's a retrofit. Bridge is designed upfront, specifically so that two hierarchies (like "shape" and "renderer") can vary independently from day one. Adapter fixes a mismatch; Bridge prevents one from ever needing to exist by keeping the two sides separate from the start.

Isn't this just dependency injection?

They overlap — Bridge is implemented using composition, the same mechanic dependency injection relies on. The distinction is intent: Bridge specifically names and separates two hierarchies that both need to grow independently, whereas "dependency injection" is a general technique that applies to passing in any collaborator, bridged hierarchy or not.

Do the two hierarchies have to be the same size?

No — that's the whole point. Three shapes and two renderers give you six usable combinations from five classes total, not six classes. The ratio can be as lopsided as the domain needs; the class count still only grows additively, not multiplicatively.

🙋 There Are No Dumb Questions

Q: Couldn't I just use an interface for the renderer and skip calling it a "pattern"?
A: That's essentially what Bridge is — an interface (or abstract class) for the varying implementation, held by reference inside the varying abstraction. The pattern's value isn't exotic machinery, it's the name and the recognition: when you notice two dimensions multiplying subclasses, "split into two hierarchies connected by composition" is a well-understood move with a name your teammates will recognize immediately.

✅ Quick Check

What problem does the Bridge pattern solve?


Next up: what if code needs to treat a single object and a whole group of objects the exact same way — no type-checks, no special cases? That's the Composite pattern.

← Back to
Next →