# Bridge

> Split a class that varies along two dimensions into two hierarchies joined by composition, so each side grows without multiplying subclasses.

- kind: pattern
- category: structural
- aliases: Handle/Body
- canonical: https://aipatterns.dev/design-patterns/bridge
- inspired-by: https://refactoring.guru/design-patterns/bridge

## 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](https://aipatterns.dev/refactoring/smells/parallel-inheritance-hierarchies.md)).

## 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

```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](https://aipatterns.dev/design-patterns/strategy.md); 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](https://aipatterns.dev/design-patterns/adapter.md), 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.

```ts title="reports.ts"
// 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");
}
```

```python title="reports.py"
import csv
import io
from abc import ABC, abstractmethod
from typing import Protocol

class Renderer(Protocol):
    def heading(self, text: str) -> str: ...
    def row(self, cells: list[str]) -> str: ...
    def document(self, parts: list[str]) -> str: ...

class TextRenderer:
    def heading(self, text: str) -> str:
        return f"{text}\n{'=' * len(text)}"

    def row(self, cells: list[str]) -> str:
        return "".join(c.ljust(20) for c in cells).rstrip()

    def document(self, parts: list[str]) -> str:
        return "\n".join(p for p in parts if p)

class CsvRenderer:
    def heading(self, text: str) -> str:
        return ""  # CSV has no headings

    def row(self, cells: list[str]) -> str:
        buf = io.StringIO()
        csv.writer(buf, lineterminator="").writerow(cells)
        return buf.getvalue()

    def document(self, parts: list[str]) -> str:
        return "\n".join(p for p in parts if p)

class Report(ABC):
    def __init__(self, out: Renderer) -> None:
        self.out = out

    @abstractmethod
    def render(self) -> str: ...

def money(cents: int) -> str:
    return f"{cents / 100:.2f}"

class InvoiceReport(Report):
    def __init__(self, out: Renderer, invoice_id: str, lines: list[tuple[str, int, int]]) -> None:
        super().__init__(out)
        self.invoice_id, self.lines = invoice_id, lines

    def render(self) -> str:
        total = sum(qty * unit for _, qty, unit in self.lines)
        return self.out.document([
            self.out.heading(f"Invoice {self.invoice_id}"),
            *(self.out.row([item, str(qty), money(qty * unit)]) for item, qty, unit in self.lines),
            self.out.row(["Total", "", money(total)]),
        ])

class UsageReport(Report):
    def __init__(self, out: Renderer, account: str, daily: list[tuple[str, int]]) -> None:
        super().__init__(out)
        self.account, self.daily = account, daily

    def render(self) -> str:
        return self.out.document([
            self.out.heading(f"API usage for {self.account}"),
            *(self.out.row([day, str(calls)]) for day, calls in self.daily),
        ])

if __name__ == "__main__":
    lines = [("Pro plan", 1, 4900), ("Extra seats", 3, 1500), ("Support, priority", 1, 9900)]
    for out in (TextRenderer(), CsvRenderer()):
        print(InvoiceReport(out, "INV-1042", lines).render(), "\n")
        print(UsageReport(out, "acme", [("2026-10-01", 18234), ("2026-10-02", 20511)]).render(), "\n")
```

```go title="reports.go"
package main

import (
	"encoding/csv"
	"fmt"
	"strings"
)

// Renderer is the implementor: primitives only.
type Renderer interface {
	Heading(text string) string
	Row(cells []string) string
	Document(parts []string) string
}

func joinNonEmpty(parts []string) string {
	kept := parts[:0:0]
	for _, p := range parts {
		if p != "" {
			kept = append(kept, p)
		}
	}
	return strings.Join(kept, "\n")
}

type TextRenderer struct{}

func (TextRenderer) Heading(t string) string { return t + "\n" + strings.Repeat("=", len(t)) }
func (TextRenderer) Row(cells []string) string {
	var b strings.Builder
	for _, c := range cells {
		fmt.Fprintf(&b, "%-20s", c)
	}
	return strings.TrimRight(b.String(), " ")
}
func (TextRenderer) Document(parts []string) string { return joinNonEmpty(parts) }

type CsvRenderer struct{}

func (CsvRenderer) Heading(string) string { return "" } // CSV has no headings
func (CsvRenderer) Row(cells []string) string {
	var b strings.Builder
	w := csv.NewWriter(&b)
	_ = w.Write(cells) // writes to memory; cannot fail
	w.Flush()
	return strings.TrimSuffix(b.String(), "\n")
}
func (CsvRenderer) Document(parts []string) string { return joinNonEmpty(parts) }

// Report is the abstraction; each struct below is a refined abstraction.
type Report interface{ Render() string }

type Line struct {
	Item      string
	Qty       int
	UnitCents int
}

func money(cents int) string { return fmt.Sprintf("%.2f", float64(cents)/100) }

type InvoiceReport struct {
	Out   Renderer
	ID    string
	Lines []Line
}

func (r InvoiceReport) Render() string {
	parts := []string{r.Out.Heading("Invoice " + r.ID)}
	total := 0
	for _, l := range r.Lines {
		total += l.Qty * l.UnitCents
		parts = append(parts, r.Out.Row([]string{l.Item, fmt.Sprint(l.Qty), money(l.Qty * l.UnitCents)}))
	}
	parts = append(parts, r.Out.Row([]string{"Total", "", money(total)}))
	return r.Out.Document(parts)
}

type UsageReport struct {
	Out     Renderer
	Account string
	Days    []string
	Calls   []int
}

func (r UsageReport) Render() string {
	parts := []string{r.Out.Heading("API usage for " + r.Account)}
	for i, day := range r.Days {
		parts = append(parts, r.Out.Row([]string{day, fmt.Sprint(r.Calls[i])}))
	}
	return r.Out.Document(parts)
}

func main() {
	lines := []Line{{"Pro plan", 1, 4900}, {"Extra seats", 3, 1500}, {"Support, priority", 1, 9900}}
	for _, out := range []Renderer{TextRenderer{}, CsvRenderer{}} {
		reports := []Report{
			InvoiceReport{Out: out, ID: "INV-1042", Lines: lines},
			UsageReport{Out: out, Account: "acme", Days: []string{"2026-10-01", "2026-10-02"}, Calls: []int{18234, 20511}},
		}
		for _, r := range reports {
			fmt.Println(r.Render() + "\n")
		}
	}
}
```

```java title="Reports.java"
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Reports {
    interface Renderer {
        String heading(String text);
        String row(List<String> cells);
        default String document(List<String> parts) {
            return parts.stream().filter(p -> !p.isEmpty()).collect(Collectors.joining("\n"));
        }
    }

    static final class TextRenderer implements Renderer {
        public String heading(String t) { return t + "\n" + "=".repeat(t.length()); }
        public String row(List<String> cells) {
            return cells.stream().map(c -> String.format("%-20s", c)).collect(Collectors.joining()).stripTrailing();
        }
    }

    static final class CsvRenderer implements Renderer {
        public String heading(String t) { return ""; } // CSV has no headings
        public String row(List<String> cells) {
            return cells.stream()
                .map(c -> c.matches("(?s).*[\",\n].*") ? "\"" + c.replace("\"", "\"\"") + "\"" : c)
                .collect(Collectors.joining(","));
        }
    }

    abstract static class Report {
        protected final Renderer out;
        Report(Renderer out) { this.out = out; }
        abstract String render();
    }

    record Line(String item, int qty, long unitCents) {}

    static String money(long cents) { return String.format("%.2f", cents / 100.0); }

    static final class InvoiceReport extends Report {
        private final String id;
        private final List<Line> lines;
        InvoiceReport(Renderer out, String id, List<Line> lines) { super(out); this.id = id; this.lines = lines; }

        String render() {
            List<String> parts = new ArrayList<>(List.of(out.heading("Invoice " + id)));
            long total = 0;
            for (Line l : lines) {
                total += l.qty() * l.unitCents();
                parts.add(out.row(List.of(l.item(), String.valueOf(l.qty()), money(l.qty() * l.unitCents()))));
            }
            parts.add(out.row(List.of("Total", "", money(total))));
            return out.document(parts);
        }
    }

    static final class UsageReport extends Report {
        private final String account;
        private final List<List<String>> daily;
        UsageReport(Renderer out, String account, List<List<String>> daily) { super(out); this.account = account; this.daily = daily; }

        String render() {
            List<String> parts = new ArrayList<>(List.of(out.heading("API usage for " + account)));
            daily.forEach(d -> parts.add(out.row(d)));
            return out.document(parts);
        }
    }

    public static void main(String[] args) {
        List<Line> lines = List.of(new Line("Pro plan", 1, 4900), new Line("Extra seats", 3, 1500),
            new Line("Support, priority", 1, 9900));
        for (Renderer out : List.of(new TextRenderer(), new CsvRenderer())) {
            System.out.println(new InvoiceReport(out, "INV-1042", lines).render() + "\n");
            System.out.println(new UsageReport(out, "acme",
                List.of(List.of("2026-10-01", "18234"), List.of("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](https://aipatterns.dev/design-patterns/adapter.md) makes unrelated existing classes fit after the fact. Bridge separates the hierarchies on purpose, before they tangle.
- [Strategy](https://aipatterns.dev/design-patterns/strategy.md) 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](https://aipatterns.dev/design-patterns/abstract-factory.md) can create a matching set of implementors (renderer, paginator, stylesheet) for one platform.
- [State](https://aipatterns.dev/design-patterns/state.md) 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.
