Bridge Pattern: Decouple Abstraction from Implementation
Split a class into two hierarchies — abstraction and implementation — so both can evolve independently. Includes Python, Java, and JavaScript examples.
Overview
The Bridge pattern is a structural design pattern that splits an abstraction from its implementation. Instead of one class hierarchy that mixes both, you get two — one for the abstraction and one for the implementation. That lets each side evolve on its own without breaking the other side.
I’ve reached for Bridge on projects where a UI framework had to support both Canvas and WebGL
backends across a dozen widget types. Without Bridge, we’d have had CanvasButton, WebGLButton,
CanvasSlider, WebGLSlider — a class explosion. With Bridge, each widget held a renderer
reference and delegated the drawing. Adding a new renderer meant one new class, not a dozen.
A typical example: a UI with different widget shapes and different rendering backends.
Without Bridge, every shape class would need to know about every renderer. With Bridge, a Circle
holds a reference to a Renderer and delegates the drawing. You can add shapes or renderers
later without breaking anything that already works. For a related take on adapting interfaces, check our
Adapter pattern — the sibling people confuse Bridge with the most.
When to Use
- You want to avoid a permanent binding between the abstraction and its implementation.
- Both sides of the design should be extensible through subclassing.
- Several objects need to share the same underlying implementation.
- Changes to the implementation shouldn’t ripple out to clients.
- You’re facing a class explosion because two dimensions, such as shapes and renderers, are combined into one hierarchy.
When NOT to Use
- A simple Strategy or Adapter already covers a single dimension of variation.
- The project is small and the extra hierarchy adds more complexity than value.
- You control neither side of the abstraction/implementation split.
Solution
Python
from abc import ABC, abstractmethod
class Renderer(ABC):
@abstractmethod
def render_circle(self, radius: float):
pass
class VectorRenderer(Renderer):
def render_circle(self, radius: float):
print(f"Drawing a circle of radius {radius} with vector graphics")
class RasterRenderer(Renderer):
def render_circle(self, radius: float):
print(f"Drawing pixels for a circle of radius {radius}")
class Shape(ABC):
def __init__(self, renderer: Renderer):
self.renderer = renderer
@abstractmethod
def draw(self):
pass
class Circle(Shape):
def __init__(self, renderer: Renderer, radius: float):
super().__init__(renderer)
self.radius = radius
def draw(self):
self.renderer.render_circle(self.radius)
circle_vector = Circle(VectorRenderer(), 5.0)
circle_vector.draw()
circle_raster = Circle(RasterRenderer(), 10.0)
circle_raster.draw()
JavaScript
class VectorRenderer {
renderCircle(radius) {
console.log(`Drawing a circle of radius ${radius} with vector graphics`);
}
}
class RasterRenderer {
renderCircle(radius) {
console.log(`Drawing pixels for a circle of radius ${radius}`);
}
}
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
draw() {
throw new Error("Subclasses must implement draw()");
}
}
class Circle extends Shape {
constructor(renderer, radius) {
super(renderer);
this.radius = radius;
}
draw() {
this.renderer.renderCircle(this.radius);
}
}
const cv = new Circle(new VectorRenderer(), 5);
cv.draw();
const cr = new Circle(new RasterRenderer(), 10);
cr.draw();
Java
public interface Renderer {
void renderCircle(double radius);
}
public class VectorRenderer implements Renderer {
public void renderCircle(double radius) {
System.out.println("Drawing a circle of radius " + radius + " with vector graphics");
}
}
public class RasterRenderer implements Renderer {
public void renderCircle(double radius) {
System.out.println("Drawing pixels for a circle of radius " + radius);
}
}
public abstract class Shape {
protected final Renderer renderer;
public Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
public class Circle extends Shape {
private final double radius;
public Circle(Renderer renderer, double radius) {
super(renderer);
this.radius = radius;
}
public void draw() {
renderer.renderCircle(radius);
}
}
Shape cv = new Circle(new VectorRenderer(), 5.0);
cv.draw();
Explanation
The pattern separates two dimensions into two class hierarchies:
- Abstraction (
Shape): the high-level interface clients use. - Implementation (
Renderer): the low-level operations that carry out the work.
The abstraction holds a reference to the implementation and delegates work to it. That separation means you can add new shapes or new renderers without touching code that already works.
Variants
| Variant | Description | Use Case |
|---|---|---|
| Classic Bridge | Two parallel hierarchies | Shapes and renderers, devices and drivers |
| Driver Bridge | Abstraction over hardware or OS APIs | Cross-platform UI frameworks |
| Remote Bridge | Local abstraction over remote implementation | RPC stubs and proxies |
Cross-platform rendering in TypeScript
interface Renderer {
renderCircle(x: number, y: number, r: number): string;
renderRect(x: number, y: number, w: number, h: number): string;
}
class SVGRenderer implements Renderer {
renderCircle(x, y, r) { return `<circle cx="${x}" cy="${y}" r="${r}" />`; }
renderRect(x, y, w, h) { return `<rect x="${x}" y="${y}" width="${w}" height="${h}" />`; }
}
class CanvasRenderer implements Renderer {
renderCircle(x, y, r) { return `ctx.arc(${x}, ${y}, ${r}, 0, Math.PI * 2); ctx.stroke();`; }
renderRect(x, y, w, h) { return `ctx.strokeRect(${x}, ${y}, ${w}, ${h});`; }
}
abstract class Shape {
constructor(protected renderer: Renderer) {}
abstract draw(): string;
}
class Circle extends Shape {
constructor(renderer: Renderer, private x: number, private y: number, private r: number) {
super(renderer);
}
draw() { return this.renderer.renderCircle(this.x, this.y, this.r); }
}
const svgCircle = new Circle(new SVGRenderer(), 50, 50, 20);
const canvasCircle = new Circle(new CanvasRenderer(), 50, 50, 20);
console.log(svgCircle.draw()); // SVG circle
console.log(canvasCircle.draw()); // Canvas circle
Remote Bridge with a local proxy
Remote Bridge puts the implementation on the other side of a network call. The abstraction lives locally and delegates to a proxy that handles the wire protocol. I’ve reached for this pattern to wrap gRPC services so application code can call them like local objects.
import json
import urllib.request
class RemoteRenderer:
"""Local abstraction over a remote rendering service."""
def __init__(self, endpoint: str):
self.endpoint = endpoint
def render_circle(self, radius: float):
payload = json.dumps({"shape": "circle", "radius": radius}).encode()
req = urllib.request.Request(
f"{self.endpoint}/render",
data=payload,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
return resp.read().decode()
# Application code doesn't know the renderer is remote
remote = RemoteRenderer("https://render.example.com")
circle = Circle(remote, 7.5)
circle.draw() # Calls the remote service transparently
The beauty here is that Circle doesn’t change at all — it still calls
self.renderer.render_circle(self.radius). The remote complexity is hidden inside the
implementation, not the abstraction.
Best Practices
- Identify the independent dimensions before applying the pattern. Not every multi-hierarchy problem needs a Bridge.
- Keep the implementation interface minimal. Expose only what the abstraction actually needs.
- Prefer composition over inheritance. The Bridge pattern is about composition.
- Use dependency injection to wire implementations into abstractions.
- Document which side is the abstraction and which side is the implementation.
Common Mistakes
- Applying Bridge when a simple Strategy or Adapter would suffice.
- Making the implementation interface too broad and coupling it to the abstraction.
- Letting the abstraction leak implementation details to clients.
- Creating deep hierarchies on both sides and reintroducing the complexity the pattern was meant to solve.
See Also
- Refactoring.Guru — Bridge Pattern — a visual, beginner-friendly explanation with analogies and a class diagram.
- Wikipedia — Bridge Pattern — the classic reference with GoF context and additional examples.
- GoF Design Patterns book — the original source for the Bridge pattern and 22 other structural, creational, and behavioral patterns.
- Strategy Pattern — our deep dive on Strategy, the pattern most often confused with Bridge.
- Adapter Pattern — our guide to Adapter, the other pattern people mix up with Bridge.
Frequently Asked Questions
What is the difference between Bridge and Adapter?
Adapter makes incompatible interfaces work together. Bridge separates an abstraction from its implementation so both can evolve independently.
When should I use Bridge instead of Strategy?
Strategy varies a single algorithm. Bridge separates two entire class hierarchies. Use Bridge when you've got two independent dimensions of variation.
Is this pattern suitable for small projects?
For small projects with few components, Bridge can add more complexity than it's worth. Start simple and pull it out when the pain gets real enough.
Can I partially apply this pattern?
Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication only where it's needed. The pattern is a guide, not a blueprint.
Why is Bridge sometimes confused with Strategy?
Both use composition to delegate work. The difference is scope: Strategy swaps one algorithm inside a single class, while Bridge separates two entire hierarchies. If you're only varying one thing, reach for Strategy. If you're varying two independent things, Bridge is the right tool.
Does Bridge work with dependency injection frameworks?
Yes, and it plays nice with them. Spring, Dagger, and similar DI containers can wire the implementation into the abstraction at runtime. That turns swapping renderers or backends into a config tweak, not a refactor.
Related Resources
Adapter Pattern
Convert the interface of a class into another interface clients expect. A structural design pattern for interface compatibility.
PatternDecorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternTwin Pattern
Provide an alternative to multiple inheritance by linking two separate classes through mutual references, allowing them to delegate methods to each other as needed.
PatternFactory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternSingleton Pattern
Ensure a class has only one instance and provide global access to it. A creational design pattern for controlled object creation.