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

# Adapter

Bridging incompatible interfaces: counting the files that touch two external carrier providers' dictionaries in the arrangement where translation spreads to the client and in the arrangement behind an adapter, comparing the edited files and added lines when a third provider is added, and deriving the pattern's cost in files, lines, and indirection.

Creational patterns solved **who constructs an object and how**: factory method gathered
type selection in one place, builder sequenced multi-step construction, dependency
injection took creation out of the using code. In each case, the object built was the
library's own, with an interface the library wrote too.

This topic addresses **how objects get combined**. The first problem: an interface whose
shape the library does not determine. A shipping fee library connects to one external
provider per carrier, and each brings its own dictionary, its own unit, its own way of
saying "I do not carry this." The **adapter** resolves this in a single place. Its gain is
measured with two numbers: files touching a provider's dictionary, and existing files
edited when a third provider is added.

## Problem: Two Dictionaries, Two Error Formats

Two external providers do the same job and agree on nothing. The first takes weight in
grams and cost in cents, and returns `null` outside its tiers.

```js
// provider/ground.mjs — external provider: works in grams and cents, returns null outside its tiers
const TIER = [[1000, 4990], [5000, 8490], [15000, 14990], [30000, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };

export function calculateCost({ weightGrams, destinationCode }) {
  const tier = TIER.find(([cap]) => weightGrams <= cap);
  if (tier === undefined) return null;
  const coefficient = COEFFICIENT[destinationCode.slice(0, 2)] ?? 165;
  return { costCents: Math.round((tier[1] * coefficient) / 100), dayCount: coefficient > 130 ? 4 : 2 };
}
```

The second works in kilograms and lira, expects a zone name instead of a postal code, and
throws for a shipment it does not carry.

```js
// provider/air.mjs — external provider: works in kilograms and lira, expects a zone name instead of a postal code, throws for out-of-coverage shipments
const ZONE_BASE = { near: 39.9, mid: 64.5, far: 98 };

export function quotePrice({ weightKg, zoneName }) {
  const base = ZONE_BASE[zoneName];
  if (base === undefined) throw new RangeError(`out of coverage: ${zoneName}`);
  if (weightKg > 20) throw new RangeError("maximum weight 20 kg");
  return {
    lira: Math.round((base + weightKg * 8.4) * 100) / 100,
    transferDays: zoneName === "far" ? 3 : 1,
  };
}
```

The library's own contract is single: a quote is a `{ carrier, amount, days }` object, the
amount in cents, and `null` for a shipment that cannot be carried. The zone table and
sample shipments are the library's own property, shared by both arrangements.

```js
// zone.mjs — the library's own zone table and sample shipments
const POSTAL_ZONE = { 34: "near", "06": "mid", 35: "mid", 65: "far" };

export const findZone = (postalCode) => POSTAL_ZONE[postalCode.slice(0, 2)] ?? "unknown";

export const SAMPLE = [
  { code: "G-1", weight: 0.8, postalCode: "34100" },
  { code: "G-2", weight: 12, postalCode: "06500" },
  { code: "G-3", weight: 26, postalCode: "65100" },
  { code: "G-4", weight: 3, postalCode: "48200" },
  { code: "G-5", weight: 8, postalCode: "35400" },
];
```

## The Arrangement Where Translation Spreads to the Client

In the first arrangement, every caller of a provider does its own translation: the module
ranking quotes carries the gram conversion, the lira conversion, and two separate error
formats in its own body.

```js
// direct/selection.mjs — selection translates both providers' dictionaries and units itself
import { calculateCost } from "../provider/ground.mjs";
import { quotePrice } from "../provider/air.mjs";
import { findZone } from "../zone.mjs";

export function quotes(shipment) {
  const out = [];
  const g = calculateCost({ weightGrams: Math.round(shipment.weight * 1000), destinationCode: shipment.postalCode });
  if (g !== null) out.push({ carrier: "ground", amount: g.costCents, days: g.dayCount });
  try {
    const a = quotePrice({ weightKg: shipment.weight, zoneName: findZone(shipment.postalCode) });
    out.push({ carrier: "air", amount: Math.round(a.lira * 100), days: a.transferDays });
  } catch (e) {
    if (!(e instanceof RangeError)) throw e;
  }
  return out.sort((a, b) => a.amount - b.amount);
}
```

A second question — how many carriers can take a shipment — spawns a second copy of the
same translation. What gets copied is not the unit but the fact that the "I do not carry
this" statement takes two different forms.

```js
// direct/coverage.mjs — the second copy of the same translation: how many carriers can carry it
import { calculateCost } from "../provider/ground.mjs";
import { quotePrice } from "../provider/air.mjs";
import { findZone } from "../zone.mjs";

export function carrierCount(shipment) {
  let count = 0;
  if (calculateCost({ weightGrams: Math.round(shipment.weight * 1000), destinationCode: shipment.postalCode }) !== null) count += 1;
  try {
    quotePrice({ weightKg: shipment.weight, zoneName: findZone(shipment.postalCode) });
    count += 1;
  } catch (e) {
    if (!(e instanceof RangeError)) throw e;
  }
  return count;
}
```

## Solution: One Adapter per Provider

The pattern's solution places, in front of each provider, an object that satisfies the
library's contract. The adapter translates three things: field names, units, and the error
format.

```js
// adapter/ground-adapter.mjs — translates the grams/cents dictionary to the library's quote contract
import { calculateCost } from "../provider/ground.mjs";

export const groundAdapter = {
  quote(shipment) {
    const g = calculateCost({ weightGrams: Math.round(shipment.weight * 1000), destinationCode: shipment.postalCode });
    return g === null ? null : { carrier: "ground", amount: g.costCents, days: g.dayCount };
  },
};
```

```js
// adapter/air-adapter.mjs — translates the lira/zone dictionary and the error to the same contract
import { quotePrice } from "../provider/air.mjs";
import { findZone } from "../zone.mjs";

export const airAdapter = {
  quote(shipment) {
    try {
      const a = quotePrice({ weightKg: shipment.weight, zoneName: findZone(shipment.postalCode) });
      return { carrier: "air", amount: Math.round(a.lira * 100), days: a.transferDays };
    } catch (e) {
      if (e instanceof RangeError) return null;
      throw e;
    }
  },
};
```

The two modules calling a provider now see a single contract and know no provider names at
all.

```js
// adapter/selection.mjs — no provider name passes through the dictionary
export const quotes = (shipment, adapters) =>
  adapters
    .map((a) => a.quote(shipment))
    .filter((q) => q !== null)
    .sort((a, b) => a.amount - b.amount);
```

```js
// adapter/coverage.mjs — counts through the single contract
import { quotes } from "./selection.mjs";

export const carrierCount = (shipment, adapters) => quotes(shipment, adapters).length;
```

```js
// adapter/main.mjs — composition root: only this file knows which adapters are used
import { groundAdapter } from "./ground-adapter.mjs";
import { airAdapter } from "./air-adapter.mjs";

export const ADAPTERS = [groundAdapter, airAdapter];
```

For the comparison to mean anything, both arrangements must give the same answer for the
same shipments.

```js
// run.mjs — do the two arrangements give the same quote order and the same carrier count?
import { SAMPLE } from "./zone.mjs";
import { quotes as directQuotes } from "./direct/selection.mjs";
import { carrierCount as directCount } from "./direct/coverage.mjs";
import { quotes as adapterQuotes } from "./adapter/selection.mjs";
import { carrierCount as adapterCount } from "./adapter/coverage.mjs";
import { ADAPTERS } from "./adapter/main.mjs";

let mismatched = 0;
for (const g of SAMPLE) {
  const a = `${JSON.stringify(directQuotes(g))} carriers=${directCount(g)}`;
  const b = `${JSON.stringify(adapterQuotes(g, ADAPTERS))} carriers=${adapterCount(g, ADAPTERS)}`;
  if (a !== b) mismatched += 1;
  console.log(`${g.code} ${a}`);
}
console.log(`mismatched results = ${mismatched} / ${SAMPLE.length}`);
```

```
G-1 [{"carrier":"air","amount":4662,"days":1},{"carrier":"ground","amount":4990,"days":2}] carriers=2
G-2 [{"carrier":"air","amount":16530,"days":1},{"carrier":"ground","amount":17239,"days":2}] carriers=2
G-3 [{"carrier":"ground","amount":36236,"days":4}] carriers=1
G-4 [{"carrier":"ground","amount":14009,"days":4}] carriers=1
G-5 [{"carrier":"air","amount":13170,"days":1},{"carrier":"ground","amount":17988,"days":2}] carriers=2
mismatched results = 0 / 5
```

Same order, same amount, same carrier count across five shipments — including an
out-of-tier weight (G-3) and a postal code missing from the table (G-4). The two
arrangements do not diverge in behavior.

## Measuring Coupling

The measure is the one used by the Keeping Framework Code at Arm's Length lesson in the
Design Principles course: files where at least one name from the external dictionary
appears. A second column is added — how many places repeat the unit conversion.

```js
// dictionary-count.mjs — counts files with a name from the provider dictionary and unit conversions
import { readdirSync, readFileSync } from "node:fs";

const DICTIONARY = ["calculateCost", "weightGrams", "destinationCode", "costCents", "dayCount",
  "quotePrice", "weightKg", "zoneName", "lira", "transferDays"];
const CONVERSION = [/\* 1000/g, /lira \* 100/g];

for (const dir of ["direct", "adapter"]) {
  const files = readdirSync(dir).filter((d) => d.endsWith(".mjs")).sort();
  let touched = 0;
  console.log(`${dir}/`);
  for (const d of files) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    const name = DICTIONARY.filter((a) => new RegExp(`\\b${a}\\b`).test(text)).length;
    const conversion = CONVERSION.reduce((t, k) => t + (text.match(k)?.length ?? 0), 0);
    if (name > 0) touched += 1;
    console.log(`  ${d.padEnd(24)} provider names=${String(name).padStart(2)}  unit conversions=${conversion}`);
  }
  console.log(`  files touching dictionary = ${touched} / ${files.length}`);
}
```

```
direct/
  coverage.mjs             provider names= 6  unit conversions=1
  selection.mjs            provider names=10  unit conversions=2
  files touching dictionary = 2 / 2
adapter/
  air-adapter.mjs          provider names= 5  unit conversions=1
  coverage.mjs             provider names= 0  unit conversions=0
  ground-adapter.mjs       provider names= 5  unit conversions=1
  main.mjs                 provider names= 0  unit conversions=0
  selection.mjs            provider names= 0  unit conversions=0
  files touching dictionary = 2 / 5
```

In the first arrangement both files touch the dictionary; in the second, two of five — the
adapters themselves; selection and coverage touch zero names. The second column also shows
the kind of coupling: unit conversion appears in three places in the first arrangement and
two in the second, and those two are the same provider's single conversion point, not a
copy of the same translation.

That lesson used the adapter as an **isolation** tool: one file translating a framework's
context object into the library's plain request shape, control running the other way. Here
the situation differs — more than one provider, control belongs to the library, and the
translation's purpose is not isolation but **comparability under a shared contract**.
Ranking five shipments' quotes in a single list is a consequence of that shared contract.

## When a Third Provider Is Added

The numbers pay off when a change arrives. The third provider resembles neither of the
first two: positional arguments, an array return, `undefined` when it does not carry the
shipment. Both trees are copied and the change applied to each.

```sh
mkdir -p new && cp -r provider zone.mjs direct adapter run.mjs new/
ls new
```

```
adapter
direct
provider
run.mjs
zone.mjs
```

```js
// new/provider/sea.mjs — third external provider: takes positional arguments, returns an array, undefined when not covered
const PORT = { 34: 1, 35: 1.1 };

export function quoteFreight(weightKg, postalCode) {
  const multiplier = PORT[postalCode.slice(0, 2)];
  if (multiplier === undefined) return undefined;
  return [Math.round((2600 + weightKg * 260) * multiplier), 6];
}
```

```js
// new/adapter/sea-adapter.mjs — translates the positional arguments and array response to the same contract
import { quoteFreight } from "../provider/sea.mjs";

export const seaAdapter = {
  quote(shipment) {
    const d = quoteFreight(shipment.weight, shipment.postalCode);
    return d === undefined ? null : { carrier: "sea", amount: d[0], days: d[1] };
  },
};
```

The only edit needed in the adapter arrangement is the list in the composition root; in the
direct arrangement, a third error format enters the body of two files. The script below
applies both edits, runs both trees, and counts the difference — the in-place edit carries
a backup extension, so GNU and BSD `sed` behave the same way.

```sh
cd new
sed -i.y -e 's#^import { findZone } from "../zone.mjs";#&\nimport { quoteFreight } from "../provider/sea.mjs";#' direct/selection.mjs direct/coverage.mjs
sed -i.y 's#^  return out.sort#  const d = quoteFreight(shipment.weight, shipment.postalCode);\n  if (d !== undefined) out.push({ carrier: "sea", amount: d[0], days: d[1] });\n&#' direct/selection.mjs
sed -i.y 's#^  return count;#  if (quoteFreight(shipment.weight, shipment.postalCode) !== undefined) count += 1;\n&#' direct/coverage.mjs
sed -i.y -e 's#^import { airAdapter } from "./air-adapter.mjs";#&\nimport { seaAdapter } from "./sea-adapter.mjs";#' \
         -e 's#airAdapter\];#airAdapter, seaAdapter];#' adapter/main.mjs
rm -f direct/*.y adapter/*.y
node run.mjs | head -2
for d in direct adapter; do
  echo "$d"
  echo "  edited existing file = $(diff -rq "../$d" "$d" | grep -c '^Files ')"
  echo "  new file             = $(diff -rq "../$d" "$d" | grep -c '^Only in ')"
  echo "  added lines          = $(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')"
done
```

```
G-1 [{"carrier":"sea","amount":2808,"days":6},{"carrier":"air","amount":4662,"days":1},{"carrier":"ground","amount":4990,"days":2}] carriers=3
G-2 [{"carrier":"air","amount":16530,"days":1},{"carrier":"ground","amount":17239,"days":2}] carriers=2
direct
  edited existing file = 2
  new file             = 0
  added lines          = 5
adapter
  edited existing file = 1
  new file             = 1
  added lines          = 9
```

Two existing files were edited in the direct arrangement, one in the adapter arrangement —
and that one is the composition root, carrying no business rule. The adapter arrangement
added more lines (9 versus 5), because the new provider's translation stands as a complete
object in its own file. The Open–Closed Principle distinction holds here too: an added line
needs no re-verification, an edited line does. The two edited bodies in the direct
arrangement also held the error paths of two already-correct providers.

## Cost and When Not to Apply It

The pattern's cost can be counted on the same scale.

```sh
for d in direct adapter; do
  echo "$d: $(ls $d/*.mjs | wc -l | tr -d ' ') files, $(cat $d/*.mjs | grep -cvE '^ *(//)?$') lines"
done
```

```
direct: 2 files, 31 lines
adapter: 5 files, 35 lines
```

Three extra files, four extra lines, plus a level of indirection. Tracing a quote means
reading two files in the direct arrangement (selection, provider) and three in the adapter
arrangement (selection, adapter, provider). Because the adapter list is injected, the
adapter does not even show up in the import graph: `adapter/selection.mjs` imports no
provider — the binding is established at run time, in the composition root. Debugging means
tracing more calls.

The pattern does not pay off in three situations. If the provider is **single** and its
contract is already under the library's control, pulling out the translation only adds a
file. If the provider's dictionary already overlaps the library's, the adapter body drops
to a layer copying field names one for one. The third bites more subtly: an adapter passes
through only what it maps. A capability with no counterpart in the contract — split
shipment, a delivery-time window — cannot be used by the business rule; using it first
requires growing the shared contract, with every adapter answering for that field. Its cost
grows in direct proportion to the provider count.

## Summary

- The problem the adapter solves is the mismatch between an interface the library does not
  determine and the library's own contract; the solution is to gather the translation in a
  single object per provider.
- Translation covers three things: field names, units, and the format of the "I do not
  carry this" statement — the last one is the most copied piece of knowledge because it is
  the difference between `null` and an error.
- The number of files touching the external dictionary is 2/2 in the direct arrangement and
  2/5 in the adapter arrangement, and the two files that touch it are the adapters
  themselves; unit conversion drops from 3 places to 2.
- When a third provider was added, 2 existing files were edited in the direct arrangement;
  in the adapter arrangement, 1 (the composition root) was edited and 1 new file was added;
  the responses for five shipments stayed the same across both arrangements.
- The cost is 3 files, 4 lines, and one level of indirection; this cost goes unpaid for when
  the provider is single, its dictionary already overlaps, or its capabilities do not fit
  the shared contract.

## Next Step

The adapter fitted an existing interface after the fact: the providers were already
written, and so was the library's contract; the pattern stepped in between the two. The
next problem shows up at the design stage itself, before anything is written. A fee report
has two independent axes: **type** (summary, tier breakdown, zone breakdown) and **output
format** (plain text, delimited values, aligned table). Combining both through inheritance
produces a class count that is the **product** of the two. The next lesson counts this
product for three types and two formats, writes the bridge pattern that splits the axes
into separate hierarchies, and compares the added type and lines across both arrangements
when a third format is added.
