Bridge
Intermediate · 11 min read · ▶ live playground · ✦ checkpoint
A shape library begins with RedCircle and BlueCircle. The moment squares arrive, it gains RedSquare and BlueSquare; every new shape must now meet every color in another subclass.
This is a Cartesian blowup, where combining n choices on one axis with m choices on another creates n × m concrete combinations. Color is one possible second axis. A rendering backend—vector or raster—creates the same pressure and gives us a useful implementation to inspect.
The Bridge pattern splits an abstraction from its implementation so both can vary independently. Composition replaces the combination subclasses, at the cost of two coordinated type families, another reference, and more construction wiring.
Before: inheritance multiplies two axes
Suppose the library must render circles and squares through vector and raster backends. An inheritance-only design puts both decisions into each class name:
abstract class Shape {
public abstract draw(): string;
}
class VectorCircle extends Shape {
public draw(): string {
return "Vector circle";
}
}
class RasterCircle extends Shape {
public draw(): string {
return "Raster circle";
}
}
class VectorSquare extends Shape {
public draw(): string {
return "Vector square";
}
}
class RasterSquare extends Shape {
public draw(): string {
return "Raster square";
}
}The four classes are not four independent ideas. They are every pairing of two shapes with two renderers. Adding Triangle requires VectorTriangle and RasterTriangle. Adding a third renderer then requires one new subclass for Circle, Square, and Triangle.
The hierarchy grows by multiplication because inheritance has fused the axes into concrete names.
After: hold the second axis behind a contract
In Bridge vocabulary, the abstraction is the high-level family clients use, here Shape and its subclasses. The implementation is the separate family that performs lower-level work, here Renderer. The bridge is the Renderer reference held by Shape, connecting the two families through composition.
“Abstraction” and “implementation” describe roles in this pattern. Shape can still be an abstract class, and both renderer classes are concrete implementations of their own interface.
class diagram
Shape and Renderer vary on opposite sides of a bridge
- 1Shape owns shape-specific state while leaving rendering details outside its hierarchy.
- 2The held Renderer reference is the bridge from the abstraction to its implementation.
- 3Circle varies the abstraction side and can work with any Renderer.
- 4Square adds another abstraction without choosing vector or raster in its class name.
- 5VectorRenderer varies the implementation side for every existing Shape.
- 6RasterRenderer joins the same contract without creating RasterCircle or RasterSquare.
- 7For n shapes and m renderers, variation classes grow by n + m instead of n × m pairings.
Shape delegates rendering but keeps the shape-specific decision about what to render. The concrete renderer decides how that request is represented.
interface Renderer {
render(shape: string, details: string): string;
}
class VectorRenderer implements Renderer {
public render(shape: string, details: string): string {
return `Vector ${shape} (${details})`;
}
}
class RasterRenderer implements Renderer {
public render(shape: string, details: string): string {
return `Raster ${shape} (${details})`;
}
}
abstract class Shape {
protected renderer: Renderer;
public constructor(renderer: Renderer) {
this.renderer = renderer;
}
public abstract draw(): string;
}
class Circle extends Shape {
private radius: number;
public constructor(renderer: Renderer, radius: number) {
super(renderer);
this.radius = radius;
}
public draw(): string {
return this.renderer.render("circle", `radius ${this.radius}`);
}
}
class Square extends Shape {
private side: number;
public constructor(renderer: Renderer, side: number) {
super(renderer);
this.side = side;
}
public draw(): string {
return this.renderer.render("square", `side ${this.side}`);
}
}
const icon: Shape = new Circle(new VectorRenderer(), 5);
const tile: Shape = new Square(new RasterRenderer(), 4);
console.log(icon.draw());
console.log(tile.draw());Vector circle (radius 5) Raster square (side 4)
The caller chooses one object from each side at runtime. new Circle(new RasterRenderer(), 5) is already a valid combination; no RasterCircle class has to exist.
If the library gains Triangle, one new Shape subclass can use both current renderers. If it gains GpuRenderer, one new implementation can serve Circle, Square, and Triangle. The varying classes grow by addition: n + m, plus the stable Shape and Renderer contracts, rather than n × m paired subclasses.
Bridge, Adapter, and Strategy
Bridge and Adapter can both place an interface in front of a composed object. Their intent and timing differ. Bridge is planned around two dimensions that should evolve independently. Adapter is introduced after an incompatible interface already exists and translates it into a target a client understands.
Strategy is a pattern that gives a client an interchangeable behavior or algorithm; its full lesson comes later. A renderer can look strategy-like because Shape can swap it. Bridge emphasizes the larger structure: an abstraction hierarchy and an implementation hierarchy evolve on opposite sides of the reference.
The same classes can sometimes be described through more than one pattern lens. Name the design pressure, not only the arrow shape. “We separated shape and renderer families to avoid their Cartesian product” is Bridge reasoning.
When NOT to use it
If Shape has one renderer and no credible second implementation axis, a Renderer interface adds indirection without containing real variation. Call the rendering function directly and wait for evidence. YAGNI still applies to pattern boundaries.
Bridge is also a poor split when the axes are not genuinely independent. If each shape requires a unique rendering protocol, forcing them through one vague render(data) method can discard type safety and move conditionals into every renderer.
The pattern's cost is permanent coordination between two hierarchies. Constructors must connect them, developers must navigate both to follow one call, and changes to the bridge contract can affect every implementation. Pay that cost when it removes a demonstrated n × m family, not because two classes happen to collaborate.
Checkpoint
Answer all three to mark this lesson complete
Bridge composes one choice from each of two families. Next, Composite uses the same interface on every level of a tree so a client can treat one object and a whole branch alike.