Design patternStructural Patterns
Bridge
Split a class that varies along two dimensions into two hierarchies joined by composition, so each side grows without multiplying subclasses.
- Complexity
- High
- Seen in the wild
- Low
- Also known as
- Handle/Body
Intent
Separate what an object does (the abstraction) from how the low-level work gets done (the implementation), and connect the two with a reference instead of inheritance. Each side gets its own hierarchy, so adding a report type or an output format is one new class, not one new class per combination.
Forces
- Two independent dimensions vs one inheritance tree: invoice and usage reports, times text and CSV output, gives
InvoiceTextReport,InvoiceCsvReport,UsageTextReport, … and the count is n × m. - High-level logic vs platform details: what goes in a report should not change when you add a new output format, and the reverse.
- Compile-time binding vs runtime choice: subclassing fixes the pairing forever; users pick the format per request.
- Shared code vs duplicated code: in a combined tree, the invoice layout is copied into every format subclass (Parallel Inheritance Hierarchies).
Therefore
Give the abstraction a field that holds an implementor interface, express the abstraction's high-level operations in terms of the implementor's primitive operations, and let both sides be subclassed independently.
The implementor interface is deliberately low level: heading, row, document. It knows nothing about invoices. The abstraction builds meaning out of those primitives. If you find yourself adding renderInvoice to the implementor, the split is in the wrong place.
Structure
Diagram source (Mermaid)
classDiagram
class Report {
<<abstract>>
-Renderer out
+render() String
}
class InvoiceReport {
+render() String
}
class UsageReport {
+render() String
}
class Renderer {
<<interface>>
+heading(text) String
+row(cells) String
+document(parts) String
}
class TextRenderer {
+heading(text) String
+row(cells) String
+document(parts) String
}
class CsvRenderer {
+heading(text) String
+row(cells) String
+document(parts) String
}
Report o-- Renderer : bridge
Report <|-- InvoiceReport
Report <|-- UsageReport
Renderer <|.. TextRenderer
Renderer <|.. CsvRendererParticipants
- Abstraction (
Report): the high-level interface clients use. Holds a reference to an implementor. - Refined abstractions (
InvoiceReport,UsageReport): variants of the high-level behavior, written only against the implementor interface. - Implementor (
Renderer): the primitive operations every platform or backend must provide. - Concrete implementors (
TextRenderer,CsvRenderer): one platform each, with no knowledge of reports. - Client: pairs an abstraction with an implementor, usually at the composition root.
Use it when
- A class hierarchy is growing in two directions at once and subclass names read like
<Thing><Platform>. - You need to pick or change the backend (renderer, storage engine, driver, OS API) at runtime, per request or per tenant.
- Both dimensions will keep growing, and you want each new member to be one class.
- You want to ship or test the high-level logic without the platform code, such as rendering reports against an in-memory recorder.
- You are designing a library that must run on several platforms with a stable public API (the classic "handle/body" split).
Do not use it when
- Only one dimension varies. One interface with several implementations is plain dependency injection or Strategy; naming it a bridge adds nothing.
- There is only one implementation and no concrete second one. An abstraction layer over a single backend is speculative generality; call the backend directly.
- The two dimensions are not independent: some combinations are invalid or need special cases. You will end up with
if (renderer instanceof CsvRenderer)in the abstraction, which defeats the split. Model the valid combinations explicitly instead. - You are integrating an existing class with the wrong interface. That is Adapter, applied after the fact. Bridge is a design decision made up front.
- The refined abstractions are just data (report = title + rows). Pass a data structure to one renderer function per format; no abstraction hierarchy is needed.
Application grill
Inspect first: the two axes of variation and evidence that both really change (the existing subclass matrix or paired switches); the codebase's stance on inheritance versus composition.
Missing pieces: none beyond the language: two small hierarchies joined by one composition edge.
Ask before you build:
- Scale: is each axis really growing, or is one dimension frozen - splitting a frozen axis is speculative generality?
- Performance: does the indirection across the join sit on a hot path?
- Reliability: when both sides hold resources, which side owns cleanup order?
- Budget: the price is two hierarchies plus wiring; who reviews that on every change?
- Implementation: are the cross-products tested table-driven, or does each combination need its own case?
Implementation steps
- Name the two dimensions. Confirm each has, or will soon have, at least two members.
- List the primitive operations the high-level side needs from the low-level side. That list is the implementor interface.
- Write one concrete implementor per platform, moving the platform-specific code out of the existing subclasses.
- Create the abstraction with a constructor that takes an implementor and stores it.
- Rewrite each high-level variant as a refined abstraction that calls only implementor primitives.
- Delete the combined subclasses, and wire pairs together where the client decides (config, request parameter, factory).
Example
A billing service exports invoices and API usage reports. Finance wants CSV, and support wants plain text in emails. Each new report or format must be one class.
// Implementor: low-level primitives, no knowledge of reports.
interface Renderer {
heading(text: string): string;
row(cells: string[]): string;
document(parts: string[]): string;
}
class TextRenderer implements Renderer {
heading(text: string) { return `${text}\n${"=".repeat(text.length)}`; }
row(cells: string[]) { return cells.map((c) => c.padEnd(20)).join("").trimEnd(); }
document(parts: string[]) { return parts.filter(Boolean).join("\n"); }
}
class CsvRenderer implements Renderer {
heading() { return ""; } // CSV has no headings
row(cells: string[]) {
return cells.map((c) => (/[",\n]/.test(c) ? `"${c.replace(/"/g, '""')}"` : c)).join(",");
}
document(parts: string[]) { return parts.filter(Boolean).join("\n"); }
}
// Abstraction: high-level meaning built from primitives.
abstract class Report {
constructor(protected readonly out: Renderer) {}
abstract render(): string;
}
const money = (cents: number) => (cents / 100).toFixed(2);
interface Line { item: string; qty: number; unitCents: number }
class InvoiceReport extends Report {
constructor(out: Renderer, private readonly id: string, private readonly lines: Line[]) {
super(out);
}
render(): string {
const total = this.lines.reduce((s, l) => s + l.qty * l.unitCents, 0);
return this.out.document([
this.out.heading(`Invoice ${this.id}`),
...this.lines.map((l) => this.out.row([l.item, String(l.qty), money(l.qty * l.unitCents)])),
this.out.row(["Total", "", money(total)]),
]);
}
}
class UsageReport extends Report {
constructor(out: Renderer, private readonly account: string, private readonly daily: [string, number][]) {
super(out);
}
render(): string {
return this.out.document([
this.out.heading(`API usage for ${this.account}`),
...this.daily.map(([day, calls]) => this.out.row([day, String(calls)])),
]);
}
}
const lines: Line[] = [
{ item: "Pro plan", qty: 1, unitCents: 4900 },
{ item: "Extra seats", qty: 3, unitCents: 1500 },
{ item: "Support, priority", qty: 1, unitCents: 9900 },
];
for (const out of [new TextRenderer(), new CsvRenderer()]) {
console.log(new InvoiceReport(out, "INV-1042", lines).render(), "\n");
console.log(new UsageReport(out, "acme", [["2026-10-01", 18234], ["2026-10-02", 20511]]).render(), "\n");
}Two reports and two formats take four classes. A third format, such as HTML, is one more class and works with every report.
Trade-offs
Gains
- Class count grows as n + m instead of n × m.
- High-level logic and platform code change independently and are tested independently.
- The implementor can be chosen or swapped at runtime.
- Platform details stay out of the client-facing API.
Costs
- The hardest part is choosing the primitives. A wrong implementor interface forces changes on every concrete implementor.
- Indirection: reading a report means reading two hierarchies.
- Capabilities only one platform has (CSV has no headings) must be expressed as no-ops or optional features.
- Premature for codebases where the second dimension never arrives.
Relations
- Adapter makes unrelated existing classes fit after the fact. Bridge separates the hierarchies on purpose, before they tangle.
- Strategy has the same delegation shape, but swaps one algorithm. Bridge splits a whole abstraction from its implementation, and both sides have their own hierarchies.
- Abstract Factory can create a matching set of implementors (renderer, paginator, stylesheet) for one platform.
- State also delegates to a changeable object, but the object changes itself as the context's situation changes.
Agent checklist
- Both dimensions have at least two real members, or the second is concretely planned.
- The implementor interface contains primitives only, with no domain words from the abstraction side.
- Refined abstractions never check the concrete implementor type.
- Every valid pairing works; invalid pairings, if any, are rejected at construction, not inside methods.
- The old combined subclasses are deleted and their tests now cover each side separately.
See also
- AdapterWrap an incompatible class in an object that exposes the interface your code expects, so you can use it without changing either side.
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
- Abstract FactoryCreate families of related objects through one interface so client code always gets a matching set and never names the concrete classes.
- StateMove each mode of an object into its own class so behavior and transitions change with the mode instead of branching on a status field.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.