Skip to content
aipatterns.dev

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 <|.. CsvRenderer

Participants

  • 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

  1. Name the two dimensions. Confirm each has, or will soon have, at least two members.
  2. List the primitive operations the high-level side needs from the low-level side. That list is the implementor interface.
  3. Write one concrete implementor per platform, moving the platform-specific code out of the existing subclasses.
  4. Create the abstraction with a constructor that takes an implementor and stores it.
  5. Rewrite each high-level variant as a refined abstraction that calls only implementor primitives.
  6. 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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.