---
title: Bridge
source: 'https://academia.sh/en/courses/design-patterns/bridge'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:15+00:00'
license: 'CC BY-SA 4.0'
---

# Bridge

Separate evolution of abstraction and implementation: comparing the arrangement where the report-type and output-format axes are multiplied through inheritance against the arrangement split into two hierarchies, by type, line, and output-body count; measuring the added type and lines when a third format is added; the cost of a product collapsing to a sum.

The adapter stepped in between two existing interfaces after the fact: the providers were
already written, and so was the library's contract; the pattern reconciled the two. This
lesson's problem shows up at the design stage, before anything is written: a capability
changes along **two independent axes** at once.

The library is going to produce a fee report. The report's **type** is one axis — summary,
weight-tier breakdown, zone breakdown — and the business rule requests a new type as it
changes. Its **output format** is the second axis: plain text, delimited values, aligned
table. The two axes grow unaware of each other; no format is tied to a particular type. The
**bridge** pattern splits them into separate hierarchies and places a contract between them.
The gain is measured in type count: combining through inheritance yields the **product** of
the type count and the format count, the bridge yields their **sum**.

## Problem: Same Data, Separate Format

The reported shipment set and the grouping helper are common to both arrangements; they are
not the axes being compared.

```js
// data.mjs — the shipment set both arrangements report on; amount is in cents
const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };
export const ZONE = { 34: "near", "06": "mid", 35: "mid", 65: "far" };

const amount = (weight, postalCode) => {
  const tier = TIER.find(([cap]) => weight <= cap) ?? [0, 24990];
  return Math.round((tier[1] * (COEFFICIENT[postalCode.slice(0, 2)] ?? 165)) / 100);
};

export const SHIPMENTS = [
  ["G-1", 0.8, "34100"], ["G-2", 12, "06500"], ["G-3", 26, "65100"],
  ["G-4", 3, "35400"], ["G-5", 4.5, "34710"], ["G-6", 18, "06800"],
].map(([code, weight, postalCode]) => ({ code, weight, postalCode, amount: amount(weight, postalCode) }));

export const TIER_ORDER = ["0-1 kg", "1-5 kg", "5-15 kg", "15+ kg"];

export const tierName = (weight) =>
  weight <= 1 ? "0-1 kg" : weight <= 5 ? "1-5 kg" : weight <= 15 ? "5-15 kg" : "15+ kg";

export const group = (shipments, key, order = null) => {
  const set = new Map();
  for (const s of shipments) {
    const k = key(s), v = set.get(k) ?? [0, 0];
    set.set(k, [v[0] + 1, v[1] + s.amount]);
  }
  const rows = [...set].map(([k, v]) => [k, String(v[0]), String(v[1])]);
  return order === null ? rows.sort() : rows.sort((x, y) => order.indexOf(x[0]) - order.indexOf(y[0]));
};
```

## The Arrangement Where the Product Unfolds into Classes

In the first arrangement, every type–format pair is a leaf class. Data extraction is
gathered in intermediate classes, and the `write` method producing output falls to the
leaves.

```js
// inheritance/reports.mjs — one leaf class per type-format pair: 3 x 2 = 6
import { ZONE, TIER_ORDER, group, tierName } from "../data.mjs";

class Report {
  constructor(shipments) { this.shipments = shipments; }
}

export class SummaryReport extends Report {
  heading() { return "summary"; }
  columns() { return ["metric", "value"]; }
  rows() {
    const t = this.shipments.reduce((s, g) => s + g.amount, 0);
    return [["shipments", String(this.shipments.length)], ["total", String(t)],
      ["average", String(Math.round(t / this.shipments.length))]];
  }
}
export class SummaryText extends SummaryReport {
  write() {
    return [this.heading(), this.columns().join("  "),
      ...this.rows().map((s) => s.join("  "))].join("\n");
  }
}
export class SummaryDelimited extends SummaryReport {
  write() {
    return [this.columns().join(";"), ...this.rows().map((s) => s.join(";"))].join("\n");
  }
}

export class TierReport extends Report {
  heading() { return "tier breakdown"; }
  columns() { return ["tier", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => tierName(g.weight), TIER_ORDER); }
}
export class TierText extends TierReport {
  write() {
    return [this.heading(), this.columns().join("  "),
      ...this.rows().map((s) => s.join("  "))].join("\n");
  }
}
export class TierDelimited extends TierReport {
  write() {
    return [this.columns().join(";"), ...this.rows().map((s) => s.join(";"))].join("\n");
  }
}

export class ZoneReport extends Report {
  heading() { return "zone breakdown"; }
  columns() { return ["zone", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => ZONE[g.postalCode.slice(0, 2)] ?? "unknown"); }
}
export class ZoneText extends ZoneReport {
  write() {
    return [this.heading(), this.columns().join("  "),
      ...this.rows().map((s) => s.join("  "))].join("\n");
  }
}
export class ZoneDelimited extends ZoneReport {
  write() {
    return [this.columns().join(";"), ...this.rows().map((s) => s.join(";"))].join("\n");
  }
}
```

The six leaves' bodies are identical two by two. This is a textbook example of the
knowledge duplication covered in the Clean Code course: the knowledge of how the
delimited-value format is produced sits in three separate classes. The caller translates
the pair into a name.

```js
// inheritance/main.mjs — composition root: a type-format pair maps directly to a leaf class
import { SummaryText, SummaryDelimited, TierText, TierDelimited, ZoneText, ZoneDelimited } from "./reports.mjs";

const LEAF = {
  "summary/text": SummaryText, "summary/delimited": SummaryDelimited,
  "tier/text": TierText, "tier/delimited": TierDelimited,
  "zone/text": ZoneText, "zone/delimited": ZoneDelimited,
};

export const report = (type, format, shipments) => new LEAF[`${type}/${format}`](shipments);
```

## Solution: Two Hierarchies, a Contract Between Them

The bridge's solution is to pull the second axis out of the inheritance tree into a tree of
its own, with the abstraction holding a **reference** to the implementation. The contract
consists of three pieces of data: heading, column names, rows.

```js
// bridge/formats.mjs — implementation hierarchy: output format lives here, report type is unknown to it
export const textFormat = {
  produce(heading, columns, rows) {
    return [heading, columns.join("  "), ...rows.map((s) => s.join("  "))].join("\n");
  },
};

export const delimitedFormat = {
  produce(heading, columns, rows) {
    return [columns.join(";"), ...rows.map((s) => s.join(";"))].join("\n");
  },
};
```

```js
// bridge/reports.mjs — abstraction hierarchy: data extraction lives here, output format lives in the format object
import { ZONE, TIER_ORDER, group, tierName } from "../data.mjs";

class Report {
  constructor(shipments, format) {
    this.shipments = shipments;
    this.format = format;
  }
  write() { return this.format.produce(this.heading(), this.columns(), this.rows()); }
}

export class SummaryReport extends Report {
  heading() { return "summary"; }
  columns() { return ["metric", "value"]; }
  rows() {
    const t = this.shipments.reduce((s, g) => s + g.amount, 0);
    return [["shipments", String(this.shipments.length)], ["total", String(t)],
      ["average", String(Math.round(t / this.shipments.length))]];
  }
}

export class TierReport extends Report {
  heading() { return "tier breakdown"; }
  columns() { return ["tier", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => tierName(g.weight), TIER_ORDER); }
}

export class ZoneReport extends Report {
  heading() { return "zone breakdown"; }
  columns() { return ["zone", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => ZONE[g.postalCode.slice(0, 2)] ?? "unknown"); }
}
```

No leaf class remains: the pair is no longer a class name but a composition of two objects,
assembled in the composition root.

```js
// bridge/main.mjs — composition root: the two axes are joined here
import { SummaryReport, TierReport, ZoneReport } from "./reports.mjs";
import { textFormat, delimitedFormat } from "./formats.mjs";

const REPORT = { summary: SummaryReport, tier: TierReport, zone: ZoneReport };
const FORMAT = { text: textFormat, delimited: delimitedFormat };

export const report = (type, format, shipments) => new REPORT[type](shipments, FORMAT[format]);
```

```js
// run.mjs — do the two arrangements produce the same text for all six type-format pairs
import { SHIPMENTS } from "./data.mjs";
import { report as inheritanceReport } from "./inheritance/main.mjs";
import { report as bridgeReport } from "./bridge/main.mjs";

let mismatched = 0;
for (const type of ["summary", "tier", "zone"]) {
  for (const format of ["text", "delimited"]) {
    if (inheritanceReport(type, format, SHIPMENTS).write() !== bridgeReport(type, format, SHIPMENTS).write()) mismatched += 1;
  }
}
console.log(inheritanceReport("zone", "text", SHIPMENTS).write());
console.log("--");
console.log(bridgeReport("zone", "delimited", SHIPMENTS).write());
console.log(`mismatched pairs = ${mismatched} / 6`);
```

```
zone breakdown
zone  count  amount
far  1  36236
mid  3  56166
near  2  13480
--
zone;count;amount
far;1;36236
mid;3;56166
near;2;13480
mismatched pairs = 0 / 6
```

The same text in all six pairs. The difference is not in the output, but in how many types
the output is split across.

## Measuring the Type and Body Count

```js
// type-count.mjs — counts the type count, line count, and output body count in both arrangements
import { readdirSync, readFileSync } from "node:fs";

const count = (text, pattern) => (text.match(pattern) ?? []).length;

for (const dir of ["inheritance", "bridge"]) {
  let type = 0, body = 0, line = 0;
  for (const d of readdirSync(dir).sort()) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    type += count(text, /^(?:export )?class \w+/gm) + count(text, /^export const \w+Format/gm);
    body += count(text, /^  write\(\)/gm) + count(text, /^  produce\(/gm);
    line += text.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length;
  }
  console.log(`${dir.padEnd(11)} type=${String(type).padStart(2)}  line=${String(line).padStart(3)}  output body=${body}`);
}
```

```
inheritance type=10  line= 63  output body=6
bridge      type= 6  line= 42  output body=3
```

For three types and two formats, inheritance carries 10 types: one base, three intermediate
classes, six leaves. The bridge carries 6: one base, three reports, two format objects.
Output-body count is 6 to 3 — one of the bridge's three is the delegating body (`write`),
two are the formats themselves. The same format body is written once per type in
inheritance, that is, 3 times.

## When a Third Format Is Added

The new requirement is an aligned table: columns are padded to the widest value. This is an
addition on the second axis; it concerns no report type's data extraction.

```sh
mkdir -p new && cp -r data.mjs inheritance bridge run.mjs type-count.mjs new/
ls new
```

```
bridge
data.mjs
inheritance
run.mjs
type-count.mjs
```

In the inheritance arrangement, avoiding writing the format body three times means pulling
it into a helper — and the moment that helper gets a name and a contract, the bridge's
implementation side is already written. The difference is that leaf classes are still
needed.

```js
// new/inheritance/aligned.mjs — third format: the body was written once but three leaf classes were still needed
import { SummaryReport, TierReport, ZoneReport } from "./reports.mjs";

const align = (heading, columns, rows) => {
  const width = columns.map((s, i) => Math.max(s.length, ...rows.map((r) => r[i].length)));
  const line = (r) => r.map((h, i) => h.padEnd(width[i])).join(" | ").trimEnd();
  return [heading, line(columns), ...rows.map(line)].join("\n");
};

export class SummaryAligned extends SummaryReport {
  write() { return align(this.heading(), this.columns(), this.rows()); }
}
export class TierAligned extends TierReport {
  write() { return align(this.heading(), this.columns(), this.rows()); }
}
export class ZoneAligned extends ZoneReport {
  write() { return align(this.heading(), this.columns(), this.rows()); }
}
```

What gets added in the bridge arrangement is a single object in the implementation
hierarchy.

```sh
cat >> new/bridge/formats.mjs <<'FORMAT'

export const alignedFormat = {
  produce(heading, columns, rows) {
    const width = columns.map((s, i) => Math.max(s.length, ...rows.map((r) => r[i].length)));
    const line = (r) => r.map((h, i) => h.padEnd(width[i])).join(" | ").trimEnd();
    return [heading, line(columns), ...rows.map(line)].join("\n");
  },
};
FORMAT
tail -3 new/bridge/formats.mjs
```

```
    return [heading, line(columns), ...rows.map(line)].join("\n");
  },
};
```

What is left is updating the two composition roots and the run script; the script below
applies these, runs all nine pairs, and counts both arrangements.

```sh
cd new
sed -i.y -e 's#^import { SummaryText.*$#&\nimport { SummaryAligned, TierAligned, ZoneAligned } from "./aligned.mjs";#' \
         -e 's#^  "zone/text": ZoneText, "zone/delimited": ZoneDelimited,#&\n  "summary/aligned": SummaryAligned, "tier/aligned": TierAligned, "zone/aligned": ZoneAligned,#' inheritance/main.mjs
sed -i.y -e 's#, delimitedFormat } from "./formats.mjs";#, delimitedFormat, alignedFormat } from "./formats.mjs";#' \
         -e 's#delimited: delimitedFormat };#delimited: delimitedFormat, aligned: alignedFormat };#' bridge/main.mjs
sed -i.y -e 's#\["text", "delimited"\]#["text", "delimited", "aligned"]#' -e 's#/ 6`#/ 9`#' \
         -e 's#bridgeReport("zone", "delimited"#bridgeReport("tier", "aligned"#' run.mjs
rm -f inheritance/*.y bridge/*.y *.y
node run.mjs | tail -7
node type-count.mjs
for d in inheritance bridge; do
  echo "$d: added lines=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')  edited files=$(diff -rq "../$d" "$d" | grep -c '^Files ')"
done
```

```
tier breakdown
tier    | count | amount
0-1 kg  | 1     | 4990
1-5 kg  | 2     | 18678
5-15 kg | 1     | 17239
15+ kg  | 2     | 64975
mismatched pairs = 0 / 9
inheritance type=13  line= 80  output body=9
bridge      type= 7  line= 49  output body=4
inheritance: added lines=17  edited files=1
bridge: added lines=9  edited files=2
```

All nine pairs produced the same text. Inheritance gained 3 types and 17 lines, the bridge 1
type and 9 lines. The two measurement points show the product: inheritance 10 → 13 (three
types times three formats plus the base and intermediate classes), bridge 6 → 7. A fourth
format brings 3 more types in inheritance, 1 more in the bridge; the difference grows not
with the format count but **with the type count**.

In edited-file count the bridge trails: 2 to 1. The bridge added the new format to an
existing implementation file, while inheritance opened a new one. The two numbers read
together — inheritance's single edited file adds three lines to the composition root, while
both files edited in the bridge are single-line lists.

## Cost and When Not to Apply It

The bridge's first cost is the contract itself. The three pieces of data between
abstraction and implementation — heading, columns, rows — are the language every format
must make do with. A format that wants to wrap a line by column width, add a subtotal, or
write a currency symbol into a cell does not fit; the contract must be grown, and a grown
contract forces **every** format object to answer for that field. The cost grows in direct
proportion to the number of implementations.

The second cost is indirection: one output is produced in a single body under inheritance,
while in the bridge the call passes from `write` to `produce`, taking the bodies to trace
from 1 to 2. The third is the loss of cell-specific behavior: any one of the six leaves can
be customized on its own under inheritance — a request for "no heading line in the zone
report's delimited output" changes a single leaf's body. In the bridge, the same request
either puts a condition checking the report type into the format object — binding
implementation to abstraction and breaking the axes' independence — or adds a flag to the
contract.

Two conditions for not applying the pattern follow from this. If one axis stays
**single-member**, the product already equals the sum, and the bridge is only a layer of
indirection. If most cells want special behavior — that is, the format is genuinely tied to
the type — the axes are not independent; in that case the bridge accumulates a condition in
every cell, and the inheritance arrangement works with fewer conditions.

## Summary

- The problem the bridge solves is a capability changing along two independent axes at
  once; the solution is moving the second axis into a separate hierarchy, with the
  abstraction holding a reference to the implementation.
- For three types and two formats, the inheritance arrangement carries 10 types and 63
  lines, the bridge arrangement 6 types and 42 lines; the output-body count is 6 to 3, and
  the same format body is written as many times as there are types in inheritance.
- A third format added 3 types and 17 lines to the inheritance arrangement, and 1 type and
  9 lines to the bridge arrangement; the output of the nine type–format pairs stayed the
  same across both arrangements.
- The two measurement points confirm the growth rule: the type count in inheritance grows
  as the product of type and format, in the bridge as their sum.
- The cost is a fixed contract every implementation must make do with, a level of
  indirection, and the loss of cell-specific behavior; the pattern does not pay off if one
  axis is single-member or the format is genuinely tied to the type.

## Next Step

Both the adapter and the bridge placed one object behind another, and in both what got
wrapped was single. The reported shipment list was flat too: six shipments, all at the same
level. The domain breaks this — a shipment can be a **consolidated** shipment carrying other
shipments inside it, and one of those can itself be consolidated. Weight total, fee total,
and delivery time are now computed over a tree. If every operation asks "is this a single
piece or consolidated" in its own body, the type check repeats once per operation. The next
lesson counts these checks, writes the composite pattern that puts the leaf and the node
behind the same interface, and measures both arrangements along two directions of growth:
files touched, lines added, and type checks added when a new node type and a new operation
are added.
