Intent

Decouple an abstraction from its implementation so that the two can vary independently. Bridge is what you reach for before you have an N×M class explosion — once you've already shipped one, you typically reach for Adapter instead.

The Problem

You're modelling shapes that need to render to multiple output targets. You start with Circle and a Square, both rendering to SVG. Then product asks for Canvas rendering. Then Print rendering. The naive class hierarchy looks like this:

Text
Shape
├── SvgCircle      ├── CanvasCircle      ├── PrintCircle
├── SvgSquare      ├── CanvasSquare      ├── PrintSquare
└── SvgTriangle    └── CanvasTriangle    └── PrintTriangle

Three shapes × three renderers = nine classes. Add a fourth shape (Hexagon) and you write three new classes. Add a fifth renderer (PdfRenderer) and you write four new classes. The class count grows as the product of two dimensions — and most of those classes are tiny variations on the same idea.

The shape of the smell: two orthogonal axes of variation (what to draw vs. how to draw it) collapsed into a single inheritance tree.

The Solution

Bridge says: split the two axes into two hierarchies and connect them at runtime. One hierarchy holds the abstraction (what — Circle, Square, Triangle). The other holds the implementation (how — SvgRenderer, CanvasRenderer). The abstraction holds a reference to an implementation and delegates the rendering primitives.

PHP
interface Renderer
{
    public function drawCircle(float $x, float $y, float $r): void;
    public function drawLine(float $x1, float $y1, float $x2, float $y2): void;
}

abstract class Shape
{
    public function __construct(protected Renderer $renderer) {}
    abstract public function draw(): void;
}

final class Circle extends Shape
{
    public function __construct(Renderer $r, private float $x, private float $y, private float $radius) {
        parent::__construct($r);
    }

    public function draw(): void
    {
        $this->renderer->drawCircle($this->x, $this->y, $this->radius);
    }
}

Now Shape and Renderer grow on their own. Adding a Hexagon is one new shape class — it works with every renderer for free. Adding a PdfRenderer is one new renderer class — every existing shape draws to PDF for free. Three shapes × three renderers requires six classes, not nine. The savings widen as either dimension grows.

Real-World Analogy

A TV remote. The remote (the abstraction — what you want done: change the channel, adjust the volume) and the TV (the implementation — how it actually happens: send an IR signal, decode it, redraw the screen) are designed to vary independently. A Sony remote can control a Samsung TV through a universal IR adapter. A new TV model doesn't require a new remote. A new remote (with voice commands, say) doesn't require redesigning the TV.

The key is the contract between them — a small, stable interface (button presses → IR codes). Each side evolves on its own axis. That's exactly what Bridge encodes in code: two hierarchies, one thin connecting interface, free composition.

Structure

Bridge pattern: a Shape abstraction holds a Renderer implementation reference; Circle and Square refine Shape; SvgRenderer and CanvasRenderer refine Renderer; the two hierarchies are independent.
Bridge: two hierarchies connected by a reference, growing independently.

Four roles you'll see in every Bridge implementation:

  • Abstraction — the high-level interface clients use. Holds a reference to an Implementor. Here: Shape.
  • Refined Abstraction — concrete subclasses of the abstraction. Here: Circle, Square, Triangle.
  • Implementor — the interface for the implementation hierarchy. Smaller than the Abstraction's interface — it exposes primitives the Abstraction composes into higher-level operations. Here: Renderer.
  • Concrete Implementor — concrete implementations. Here: SvgRenderer, CanvasRenderer, PrintRenderer.

The defining property: the Abstraction's API is what clients use. The Implementor's API is what the abstraction uses internally. They're two layered languages — and that layering is what lets each evolve.

Code Examples

Here's a Shape × Renderer Bridge in five languages. Watch how Circle doesn't know what kind of renderer it has — only that it can call drawCircle and drawLine.

interface Renderer {
  drawCircle(x: number, y: number, r: number): void;
  drawLine(x1: number, y1: number, x2: number, y2: number): void;
}

abstract class Shape {
  constructor(protected renderer: Renderer) {}
  abstract draw(): void;
}

class Circle extends Shape {
  constructor(renderer: Renderer, private x: number, private y: number, private radius: number) {
    super(renderer);
  }

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

class Square extends Shape {
  constructor(renderer: Renderer, private x: number, private y: number, private side: number) {
    super(renderer);
  }

  draw(): void {
    const { x, y, side } = this;
    this.renderer.drawLine(x, y, x + side, y);
    this.renderer.drawLine(x + side, y, x + side, y + side);
    this.renderer.drawLine(x + side, y + side, x, y + side);
    this.renderer.drawLine(x, y + side, x, y);
  }
}

class SvgRenderer implements Renderer { /* ... */ drawCircle()=>{} drawLine()=>{} }
class CanvasRenderer implements Renderer { /* ... */ drawCircle()=>{} drawLine()=>{} }

// Mix and match freely:
new Circle(new SvgRenderer(), 10, 10, 5).draw();
new Square(new CanvasRenderer(), 0, 0, 20).draw();
from abc import ABC, abstractmethod

class Renderer(ABC):
    @abstractmethod
    def draw_circle(self, x, y, r): ...
    @abstractmethod
    def draw_line(self, x1, y1, x2, y2): ...

class Shape(ABC):
    def __init__(self, renderer):
        self.renderer = renderer

    @abstractmethod
    def draw(self): ...

class Circle(Shape):
    def __init__(self, renderer, x, y, radius):
        super().__init__(renderer)
        self.x, self.y, self.radius = x, y, radius

    def draw(self):
        self.renderer.draw_circle(self.x, self.y, self.radius)

class Square(Shape):
    def __init__(self, renderer, x, y, side):
        super().__init__(renderer)
        self.x, self.y, self.side = x, y, side

    def draw(self):
        x, y, s = self.x, self.y, self.side
        self.renderer.draw_line(x,     y,     x + s, y)
        self.renderer.draw_line(x + s, y,     x + s, y + s)
        self.renderer.draw_line(x + s, y + s, x,     y + s)
        self.renderer.draw_line(x,     y + s, x,     y)

class SvgRenderer(Renderer):
    def draw_circle(self, x, y, r): pass
    def draw_line(self, x1, y1, x2, y2): pass

class CanvasRenderer(Renderer):
    def draw_circle(self, x, y, r): pass
    def draw_line(self, x1, y1, x2, y2): pass
public interface Renderer {
    void drawCircle(double x, double y, double r);
    void drawLine(double x1, double y1, double x2, double y2);
}

public abstract class Shape {
    protected final Renderer renderer;
    protected Shape(Renderer renderer) { this.renderer = renderer; }
    public abstract void draw();
}

public final class Circle extends Shape {
    private final double x, y, radius;

    public Circle(Renderer r, double x, double y, double radius) {
        super(r);
        this.x = x; this.y = y; this.radius = radius;
    }

    @Override
    public void draw() {
        renderer.drawCircle(x, y, radius);
    }
}

public final class Square extends Shape {
    private final double x, y, side;

    public Square(Renderer r, double x, double y, double side) {
        super(r);
        this.x = x; this.y = y; this.side = side;
    }

    @Override
    public void draw() {
        renderer.drawLine(x,        y,        x + side, y);
        renderer.drawLine(x + side, y,        x + side, y + side);
        renderer.drawLine(x + side, y + side, x,        y + side);
        renderer.drawLine(x,        y + side, x,        y);
    }
}
<?php

namespace App\Graphics;

interface Renderer
{
    public function drawCircle(float $x, float $y, float $r): void;
    public function drawLine(float $x1, float $y1, float $x2, float $y2): void;
}

abstract class Shape
{
    public function __construct(protected Renderer $renderer) {}
    abstract public function draw(): void;
}

final class Circle extends Shape
{
    public function __construct(Renderer $r, private float $x, private float $y, private float $radius)
    {
        parent::__construct($r);
    }

    public function draw(): void
    {
        $this->renderer->drawCircle($this->x, $this->y, $this->radius);
    }
}

final class Square extends Shape
{
    public function __construct(Renderer $r, private float $x, private float $y, private float $side)
    {
        parent::__construct($r);
    }

    public function draw(): void
    {
        $x = $this->x; $y = $this->y; $s = $this->side;
        $this->renderer->drawLine($x,      $y,      $x + $s, $y);
        $this->renderer->drawLine($x + $s, $y,      $x + $s, $y + $s);
        $this->renderer->drawLine($x + $s, $y + $s, $x,      $y + $s);
        $this->renderer->drawLine($x,      $y + $s, $x,      $y);
    }
}
package graphics

type Renderer interface {
    DrawCircle(x, y, r float64)
    DrawLine(x1, y1, x2, y2 float64)
}

type Shape interface {
    Draw()
}

type Circle struct {
    Renderer Renderer
    X, Y, R  float64
}

func (c Circle) Draw() {
    c.Renderer.DrawCircle(c.X, c.Y, c.R)
}

type Square struct {
    Renderer Renderer
    X, Y, S  float64
}

func (s Square) Draw() {
    s.Renderer.DrawLine(s.X,     s.Y,     s.X+s.S, s.Y)
    s.Renderer.DrawLine(s.X+s.S, s.Y,     s.X+s.S, s.Y+s.S)
    s.Renderer.DrawLine(s.X+s.S, s.Y+s.S, s.X,     s.Y+s.S)
    s.Renderer.DrawLine(s.X,     s.Y+s.S, s.X,     s.Y)
}

type SvgRenderer    struct{}
type CanvasRenderer struct{}

func (SvgRenderer)    DrawCircle(x, y, r float64)         { /* ... */ }
func (SvgRenderer)    DrawLine(x1, y1, x2, y2 float64)    { /* ... */ }
func (CanvasRenderer) DrawCircle(x, y, r float64)         { /* ... */ }
func (CanvasRenderer) DrawLine(x1, y1, x2, y2 float64)    { /* ... */ }

The key reading: Square.draw() doesn't know what kind of renderer is doing the work. It just speaks the small primitive language (drawLine, drawCircle) and trusts the renderer to handle the rest. Adding a PdfRenderer requires one new class, and every shape draws to PDF immediately.

When to Use It

Reach for Bridge when you can answer "yes" to any of these:

  • You're designing a system upfront with two orthogonal axes of variation. Cross-platform UI, document formats × storage backends, database engines × query builders, devices × drivers.
  • You want to mix and match at runtime — pick a shape, pick a renderer, compose them. Bridge is composition, not inheritance.
  • The Implementor side is genuinely platform-specific — different operating systems, different vendor APIs, different output formats — and you want one Abstraction to ride on top of all of them.
  • You expect both sides to grow. If only the Implementor will grow, a single interface is enough. If only the Abstraction will grow, a single base class is enough. Bridge is for when both will grow.

If you only ever have one Implementor in production and the second is hypothetical, you're paying the cost without the benefit. Wait until the second one is real.

Pros and Cons

Pros

  • The class count grows as N+M instead of N×M.
  • Both hierarchies evolve independently — adding a shape doesn't touch any renderer; adding a renderer doesn't touch any shape.
  • Implementations can be swapped at runtime — pick a renderer based on output target.
  • The Implementor interface is small and stable — easy to add new platforms behind it.

Cons

  • Designing the Implementor interface well is hard upfront. Pick the wrong primitives and the Abstractions become awkward. This is why Bridge is hard to retrofit — you usually need to rethink both sides.
  • Two hierarchies are more code than one for tiny systems. If you only have two shapes and one renderer, you don't need the indirection.
  • Confusing to readers who don't know the pattern. "Why does Circle take a Renderer in its constructor?" needs an explanation.
  • The pattern is often implicit in modern frameworks (a View and a ViewModel, an ORM and its drivers, a logger and its handlers) — recognising "this is Bridge" matters more than writing one from scratch.

Pro Tips

  • Keep the Implementor interface small and primitive. Drawing primitives like drawLine and drawCircle are good. A drawSquareWithBorderAndShadow is leaking abstraction-level concerns down.
  • Design the Implementor interface from the Abstractions' needs. What primitives do you need to compose every shape? Start there. Don't model the implementor's underlying technology.
  • Bridge is a design-up-front pattern. If you didn't anticipate the second axis, Adapter is what you reach for to retrofit. Bridge feels overkill until you actually need both axes — and then it feels obvious.
  • Compose Bridges with Abstract Factory. When you need consistent families of Implementors (an SVG renderer + an SVG color palette + an SVG font system), Abstract Factory produces them; Bridge connects them to the abstractions.
  • Watch for "Bridge that's secretly Strategy." If your Implementor is just "swap in a different algorithm at runtime," you have Strategy, not Bridge. Bridge is structural — it splits a taxonomy. Strategy is behavioral — it swaps a behavior.

Relations with Other Patterns

  • Adapter retrofits two existing interfaces to work together; Bridge designs them apart from the start to vary independently. Both wrap, but with opposite intents and timing.
  • Strategy is the behavioral cousin: Strategy swaps an algorithm at runtime; Bridge swaps an entire implementation hierarchy. Same composition shape, different scope.
  • Abstract Factory can produce coherent families of Implementors when a single Abstraction needs several coordinated implementor instances.
  • Composite can host Bridges — a tree of Shapes, each holding its own Renderer, drawn uniformly.
  • State is a special case of Bridge where the Implementor changes on the fly to reflect the Abstraction's state.

Final Tips

The cleanest Bridge I've ever shipped was a notification system that supported three channels (email, SMS, push) and three urgency levels (info, warning, alert). The naive design wanted nine classes (InfoEmail, WarningSms, etc.). Bridge gave us two hierarchies — Notification (with Info, Warning, Alert refinements) and Channel (with Email, Sms, Push implementations) — for six classes total. Adding a fourth channel (Slack) added one class; every urgency level worked through it instantly.

The pattern is one of the harder GoF patterns to recognize because it looks identical to Strategy on a UML diagram. The difference is intent: Bridge separates a taxonomy, where both sides are real hierarchies that grow over time. Strategy swaps an algorithm, where one side is the user and the other is a swappable behavior. Reach for Bridge when you can name both axes — and you can already see both growing.