---
title: 'Programming to Abstractions'
source: 'https://academia.sh/en/courses/design-principles/programming-to-abstractions'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:17+00:00'
license: 'CC BY-SA 4.0'
---

# Programming to Abstractions

Counting the cost of binding to a concrete type: comparing the number of files edited and lines touched in two layouts when a second price list is added, finding the number of places that make the choice, and showing that a fake price list only takes effect for the client bound to an abstraction.

In the previous lesson's isolated layout, all three clients imported the price module by name
and called its function directly. As long as there was only one price list, this stayed
invisible. When a second list arrives for contracted customers, the question sharpens: how
will the client know which list to use?

**Programming to abstractions** means the client binds to a contract that multiple
implementations can satisfy, not to a concrete module. The dependency inversion lesson
measured which package the contract is **defined** in; the measure here is on the client
side: how many lines are touched when the implementation changes, or when a second one is
added?

## Layout Bound to a Concrete Module

```sh
mkdir -p concrete abstract
```

```js
// records.mjs — sample shipments used by both layouts
export const RECORDS = [
  { code: "GN-1", weight: 0.8, address: "34100", contracted: false },
  { code: "GN-2", weight: 3.0, address: "06500", contracted: true },
  { code: "GN-3", weight: 12.0, address: "65200", contracted: true },
];
```

```js
// concrete/price.mjs — the standard price list
const TIER = [[1, 3900], [5, 6400], [20, 11800]];
const ZONE = { "34": 100, "06": 115, "65": 140 };

export function calculateCents(shipment) {
  const base = TIER.find(([max]) => shipment.weight <= max)?.[1] ?? 15000;
  return Math.round((base * (ZONE[shipment.address.slice(0, 2)] ?? 160)) / 100);
}
```

```js
// concrete/web.mjs — imports the concrete price module by name
import { calculateCents } from "./price.mjs";

export const line = (shipment) => `${shipment.code} ${calculateCents(shipment)} cents`;
```

```js
// concrete/batch.mjs — imports the concrete price module by name
import { calculateCents } from "./price.mjs";

export const lines = (shipments) =>
  shipments.map((s) => `${s.code} ${calculateCents(s)} cents`);
```

```js
// concrete/report.mjs — imports the concrete price module by name
import { calculateCents } from "./price.mjs";

export const totalLine = (shipments) =>
  `TOTAL ${shipments.reduce((t, s) => t + calculateCents(s), 0)} cents`;
```

```js
// concrete/setup.mjs — composition root
import { RECORDS } from "../records.mjs";
import { line } from "./web.mjs";
import { lines } from "./batch.mjs";
import { totalLine } from "./report.mjs";

console.log(line(RECORDS[1]));
console.log(lines(RECORDS).join(" | "));
console.log(totalLine(RECORDS));
```

## Layout Bound to an Abstraction

In the second layout, the price list is an object given to the clients from outside. The
clients know that a method named `calculateCents` exists; they do not know which list is
arriving.

```js
// abstract/price-standard.mjs — a standard list that satisfies the contract
const TIER = [[1, 3900], [5, 6400], [20, 11800]];
const ZONE = { "34": 100, "06": 115, "65": 140 };

export const standard = {
  name: "standard",
  calculateCents(shipment) {
    const base = TIER.find(([max]) => shipment.weight <= max)?.[1] ?? 15000;
    return Math.round((base * (ZONE[shipment.address.slice(0, 2)] ?? 160)) / 100);
  },
};
```

```js
// abstract/web.mjs — takes the price from outside, does not know the implementation's name
export const line = (price, shipment) => `${shipment.code} ${price.calculateCents(shipment)} cents`;
```

```js
// abstract/batch.mjs — takes the price from outside
export const lines = (selectPrice, shipments) =>
  shipments.map((s) => `${s.code} ${selectPrice(s).calculateCents(s)} cents`);
```

```js
// abstract/report.mjs — takes the price from outside
export const totalLine = (selectPrice, shipments) =>
  `TOTAL ${shipments.reduce((t, s) => t + selectPrice(s).calculateCents(s), 0)} cents`;
```

```js
// abstract/setup.mjs — composition root: only this file knows which price list is used
import { RECORDS } from "../records.mjs";
import { standard } from "./price-standard.mjs";
import { line } from "./web.mjs";
import { lines } from "./batch.mjs";
import { totalLine } from "./report.mjs";

const selectPrice = () => standard;

console.log(line(selectPrice(RECORDS[1]), RECORDS[1]));
console.log(lines(selectPrice, RECORDS).join(" | "));
console.log(totalLine(selectPrice, RECORDS));
```

```sh
node concrete/setup.mjs
node abstract/setup.mjs
```

```
GN-2 7360 cents
GN-1 3900 cents | GN-2 7360 cents | GN-3 16520 cents
TOTAL 27780 cents
GN-2 7360 cents
GN-1 3900 cents | GN-2 7360 cents | GN-3 16520 cents
TOTAL 27780 cents
```

## Second Implementation

A contracted customer list arrives: the tier fees and zone factors differ. Both trees are
copied and the requirement is applied to both.

```sh
cp -r concrete concrete-new
cp -r abstract abstract-new
```

```js
// concrete-new/price-contracted.mjs — contracted customer price list
const TIER = [[1, 3200], [5, 5300], [20, 9700]];
const ZONE = { "34": 100, "06": 110, "65": 130 };

export function calculateCents(shipment) {
  const base = TIER.find(([max]) => shipment.weight <= max)?.[1] ?? 13000;
  return Math.round((base * (ZONE[shipment.address.slice(0, 2)] ?? 150)) / 100);
}
```

```js
// concrete-new/web.mjs — imports both concrete modules and chooses between them
import { calculateCents } from "./price.mjs";
import { calculateCents as contractedCents } from "./price-contracted.mjs";

export const line = (shipment) =>
  `${shipment.code} ${shipment.contracted ? contractedCents(shipment) : calculateCents(shipment)} cents`;
```

```js
// concrete-new/batch.mjs — the same choice a second time
import { calculateCents } from "./price.mjs";
import { calculateCents as contractedCents } from "./price-contracted.mjs";

export const lines = (shipments) =>
  shipments.map((s) =>
    `${s.code} ${s.contracted ? contractedCents(s) : calculateCents(s)} cents`);
```

```js
// concrete-new/report.mjs — the same choice a third time
import { calculateCents } from "./price.mjs";
import { calculateCents as contractedCents } from "./price-contracted.mjs";

export const totalLine = (shipments) =>
  `TOTAL ${shipments.reduce((t, s) =>
    t + (s.contracted ? contractedCents(s) : calculateCents(s)), 0)} cents`;
```

The clients are not opened in the abstract layout. The new list is a second object that
satisfies the same contract, and the choice is made in the composition root.

```js
// abstract-new/price-contracted.mjs — a second list satisfying the same contract
const TIER = [[1, 3200], [5, 5300], [20, 9700]];
const ZONE = { "34": 100, "06": 110, "65": 130 };

export const contracted = {
  name: "contracted",
  calculateCents(shipment) {
    const base = TIER.find(([max]) => shipment.weight <= max)?.[1] ?? 13000;
    return Math.round((base * (ZONE[shipment.address.slice(0, 2)] ?? 150)) / 100);
  },
};
```

```js
// abstract-new/setup.mjs — composition root: only this file knows which price list is used
import { RECORDS } from "../records.mjs";
import { standard } from "./price-standard.mjs";
import { contracted } from "./price-contracted.mjs";
import { line } from "./web.mjs";
import { lines } from "./batch.mjs";
import { totalLine } from "./report.mjs";

const selectPrice = (s) => (s.contracted ? contracted : standard);

console.log(line(selectPrice(RECORDS[1]), RECORDS[1]));
console.log(lines(selectPrice, RECORDS).join(" | "));
console.log(totalLine(selectPrice, RECORDS));
```

## Counting the Cost

```sh
node concrete-new/setup.mjs
node abstract-new/setup.mjs
for k in concrete abstract; do
  echo "$k: files edited = $(diff -rq $k $k-new | grep -c '^Files')" \
    " new files = $(diff -rq $k $k-new | grep -c '^Only in')" \
    " lines touched = $(diff -rU0 $k $k-new | grep -cE '^[+-][^+-]')" \
    " files making the choice = $(grep -l 'contracted ?' $k-new/*.mjs | wc -l | tr -d ' ')"
done
```

```
GN-2 5830 cents
GN-1 3900 cents | GN-2 5830 cents | GN-3 12610 cents
TOTAL 22340 cents
GN-2 5830 cents
GN-1 3900 cents | GN-2 5830 cents | GN-3 12610 cents
TOTAL 22340 cents
concrete: files edited = 3  new files = 1  lines touched = 18  files making the choice = 3
abstract: files edited = 1  new files = 1  lines touched = 5  files making the choice = 1
```

Same requirement, same result, different cost: eighteen lines against five, three files
edited against one. The last column gives the reason for the difference — in the concrete
layout, the choice is made in three separate places. When a third list is added, all three of
those places will be edited again; in the abstract layout, the place to edit will remain the
composition root.

## Substitutability

The second measure is whether the implementation can be swapped out during testing. The same
client is given a fake price list that produces a fixed value.

```js
// try-fake.mjs — can clients work with a fake price list
const FAKE = { name: "fake", calculateCents: () => 1000 };
const G = { code: "GN-9", weight: 3.0, address: "06500", contracted: false };

const concreteWeb = await import("./concrete/web.mjs");
const abstractWeb = await import("./abstract/web.mjs");

console.log("concrete ->", concreteWeb.line(G));
console.log("abstract ->", abstractWeb.line(FAKE, G));
```

```sh
node try-fake.mjs
```

```
concrete -> GN-9 7360 cents
abstract -> GN-9 1000 cents
```

In the concrete layout, there is no place to hand the fake list to; the client ignored it and
calculated the real price. In the abstract layout, the same client used the fake. The
difference is not a convenience, it is structural: a concrete import leaves no point of
choice.

## The Cost of the Principle

Abstraction is not free. In the abstract layout, all three clients' signatures grew by one
parameter, and the `selectPrice` function built an extra layer of indirection in the
composition root. If the implementation stays single, this extra cost is never recovered: the
measurement paid off once a second implementation arrived; had none arrived, it would only
have increased the parameter count.

The criterion is therefore the history of implementation count. If a module has never needed
a second implementation, and testing does not require swapping it either, a concrete import
is the right choice. If testing requires swapping it, the implementation count is already
two; a fake is an implementation too.

## Summary

- Programming to abstractions means the client binds to a contract that multiple
  implementations can satisfy, not to a concrete module.
- When the second price list was added, the concrete layout touched 3 files and 18 lines, the
  abstract layout touched 1 file and 5 lines.
- The source of the difference is the point of choice: the concrete layout makes the choice
  in 3 files, the abstract layout in 1.
- A fake price list took effect only in the abstract layout; a concrete import leaves no
  point at which the implementation can be swapped.
- The cost of the abstraction is the parameter count and a layer of indirection; if a second
  implementation never arrives, this cost is never recovered.

## Next Step

In the abstract layout, the composition root decides which price list is used, but it still
calls the clients itself: first `line`, then `lines`, then `totalLine`. The order and count of
the steps live in the caller's body. Adding a new step to this flow — a contracted customer
discount, an insurance premium, an audit line — requires editing the caller every time. The
next lesson reverses the direction between caller and callee: it moves the order of the steps
into a skeleton, moves the steps themselves into parts handed to the skeleton, and compares
the cost of adding a new step in the two arrangements.
