---
title: 'You Are Not Going to Need It'
source: 'https://academia.sh/en/courses/clean-code/you-arent-gonna-need-it'
course: 'Clean Code'
language: en
updated: '2026-08-23T07:01:12+00:00'
license: 'CC BY-SA 4.0'
---

# You Are Not Going to Need It

Measuring an extension point and a configuration layer added to the fee calculation today by their decision-point count, branch coverage, and unexercised-path count; showing with a failing run that the incoming requirement does not fit that structure, and discarding the generalization.

In the previous lesson, every step answered a requirement that had already arrived,
and the four rules were met in five steps. What structure written today for a
requirement that has not arrived costs was not measured. This lesson adds an extension
point and a configuration layer to the fee calculation today, counts the decision
points and never-exercised paths the two produce, and then shows that the requirement
that actually arrives does not fit that structure.

The principle's name is **you are not going to need it**: a capability does not get
written until the need for it actually shows up. The principle is a ban on
prediction — not a claim that a developer cannot predict a future requirement
correctly, but a statement that the **cost of a wrong prediction** is measurable.

The starting point is the previous lesson's last step.

```js
// tariff.mjs — single source of tier data
export const TIER = [
  { maxGrams: 1000, cents: 4990, name: 'small' },
  { maxGrams: 5000, cents: 7490, name: 'medium' },
  { maxGrams: 10000, cents: 9900, name: 'large' },
  { maxGrams: 20000, cents: 12900, name: 'extra-large' },
  { maxGrams: Infinity, cents: 24900, name: 'heavy' },
];

export function tier(grams) {
  return TIER.find((t) => grams <= t.maxGrams);
}
```

```js
// fee.mjs — carried over from the previous lesson's last step
import { tier } from './tariff.mjs';

const ZONE_FACTOR = { B1: 1.0, B2: 1.25, B3: 1.6 };
const MINIMUM_CENTS = 6990;

export function effectiveWeight(s) {
  return Math.max(s.grams, Math.ceil((s.width * s.length * s.height) / 3));
}

export function shipmentFee(s) {
  const withZoneFactor = Math.round(tier(effectiveWeight(s)).cents * ZONE_FACTOR[s.zone]);
  return Math.max(withZoneFactor, MINIMUM_CENTS);
}
```

```js
// fee.test.mjs — four tests carried over from the previous lesson
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { shipmentFee } from './fee.mjs';

const box = { width: 20, length: 15, height: 10 };

test('the tier fee is read from the weight', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B1' }), 7490);
});

test('the zone factor multiplies the base fee', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B3' }), 11984);
});

test('the minimum fee is applied after the zone multiplication', () => {
  assert.equal(shipmentFee({ ...box, grams: 500, zone: 'B2' }), 6990);
});

test('volumetric weight is used when it exceeds actual weight', () => {
  assert.equal(shipmentFee({ width: 40, length: 30, height: 30, grams: 500, zone: 'B2' }), 16125);
});
```

The measurement needs a counter: a script that counts a file's **decision points** —
the places where the program picks one of two separate paths.

```js
// branches.mjs — counts decision points in the given files
import { readFileSync } from 'node:fs';

const PATTERN = /\bif\b|\bcase\b|\?\?|&&|\|\||\?\.|\?(?![?.])/g;

let total = 0;
for (const path of process.argv.slice(2)) {
  const n = (readFileSync(path, 'utf8').match(PATTERN) ?? []).length;
  console.log(`${path}: ${n} decision points`);
  total += n;
}
console.log(`total: ${total}`);
```

```sh
node --test --test-reporter=tap fee.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node branches.mjs fee.mjs
```

```
ok 1 - the tier fee is read from the weight
ok 2 - the zone factor multiplies the base fee
ok 3 - the minimum fee is applied after the zone multiplication
ok 4 - volumetric weight is used when it exceeds actual weight
# tests 4
# pass 4
# fail 0
fee.mjs: 0 decision points
total: 0
```

## Two Kinds of Flexibility Written In Today

The tariff rules will change over time. That is a correct observation and a wrong
justification: knowing that change is coming does not mean knowing what shape it will
take. Two structures get written in today regardless. The first is a configuration
layer that makes every detail of the calculation selectable from outside.

```js
// configuration.mjs — configuration layer written in today
export const DEFAULTS = {
  applyVolume: true,
  volumeDivisor: 3,
  applyMinimum: true,
  minimumCents: 6990,
  rounding: 'nearest',
  factorSource: 'fixed',
};

export function configure(options = {}) {
  return { ...DEFAULTS, ...options };
}
```

The second is an **extension point** that lets future fee rules plug into the
calculation from outside.

```js
// extension.mjs — extension point for fee rules
const rules = [];

export function addRule(name, priority, apply) {
  rules.push({ name, priority, apply });
  rules.sort((a, b) => a.priority - b.priority);
}

export function resetRules() {
  rules.length = 0;
}

export function applyRules(cents, s, config) {
  let result = cents;
  for (const r of rules) {
    result = r.apply(result, s, config);
  }
  return result;
}
```

The fee calculation is rewritten to use both. Behavior stays the same: with the
default configuration and no registered rule, the results do not change.

```js
// fee.mjs — version generalized in today
import { tier } from './tariff.mjs';
import { configure } from './configuration.mjs';
import { applyRules } from './extension.mjs';

const FIXED_FACTOR = { B1: 1.0, B2: 1.25, B3: 1.6 };

function factor(s, config) {
  if (config.factorSource === 'external') return config.factorTable?.[s.zone] ?? 1;
  return FIXED_FACTOR[s.zone] ?? 1;
}

function round(cents, config) {
  if (config.rounding === 'none') return cents;
  if (config.rounding === 'up') return Math.ceil(cents);
  return Math.round(cents);
}

export function effectiveWeight(s, config) {
  if (!config.applyVolume) return s.grams;
  return Math.max(s.grams, Math.ceil((s.width * s.length * s.height) / config.volumeDivisor));
}

export function shipmentFee(s, options = {}) {
  const config = configure(options);
  const withZoneFactor = round(tier(effectiveWeight(s, config)).cents * factor(s, config), config);
  const withMinimum = config.applyMinimum ? Math.max(withZoneFactor, config.minimumCents) : withZoneFactor;
  return applyRules(withMinimum, s, config);
}
```

## The Cost Paid, Measured

The tests did not change and stay green. What changed is the size of the structure
the tests are able to **cover**. Alongside decision points, branch coverage — which
shows which paths the run actually goes through — is measured too.

```sh
node --test --test-reporter=tap fee.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node branches.mjs fee.mjs configuration.mjs extension.mjs
node --test --experimental-test-coverage --test-reporter=tap fee.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
```

```
ok 1 - the tier fee is read from the weight
ok 2 - the zone factor multiplies the base fee
ok 3 - the minimum fee is applied after the zone multiplication
ok 4 - volumetric weight is used when it exceeds actual weight
# tests 4
# pass 4
# fail 0
fee.mjs: 8 decision points
configuration.mjs: 0 decision points
extension.mjs: 0 decision points
total: 8
# start of coverage report
# ------------------------------------------------------------------
# file              | line % | branch % | funcs % | uncovered lines
# ------------------------------------------------------------------
# configuration.mjs | 100.00 |   100.00 |  100.00 | 
# extension.mjs     |  63.16 |    66.67 |   33.33 | 5-7 10-11 16-17
# fee.mjs           | 100.00 |    45.45 |  100.00 | 
# tariff.mjs        | 100.00 |   100.00 |  100.00 | 
# ------------------------------------------------------------------
# all files         |  90.41 |    63.16 |   80.00 | 
# ------------------------------------------------------------------
# end of coverage report
```

Three numbers are worth reading. The decision-point count rose from zero to eight;
none of them comes from a domain rule, all of them test which value an option holds.
The fee file's line coverage is 100 percent, but its branch coverage is 45.45 percent:
every line runs, but more than half of the paths do not. In the extension file,
`addRule` and `resetRules` are never called, so function coverage stays at 33.33
percent, and the uncovered lines are listed one by one.

There is no way around these paths staying unmaintained: raising branch coverage to
100 percent means writing at least one more test for each of the six extra decision
points. No call site today wants the behavior those tests would exercise. The
configuration layer carries six fields, and the number of options any call site passes
is zero; the number of rules registered on the extension list is zero too.

## Incoming Requirement

A rule arrives for contract customers: a customer with more than fifty shipments in a
billing period gets a twelve percent discount on **every shipment in that period**.
The extension point was written exactly for rules like this one; the rule tries to
plug in there.

```js
// period.mjs — attempt to write the period discount through the extension point
import { shipmentFee } from './fee.mjs';
import { addRule, resetRules } from './extension.mjs';

const THRESHOLD = 50;
const RATE = 0.88;

export function periodFee(shipments) {
  resetRules();
  let count = 0;
  addRule('period-discount', 10, (cents) => {
    count += 1;
    return count > THRESHOLD ? Math.round(cents * RATE) : cents;
  });
  return shipments.reduce((t, s) => t + shipmentFee(s), 0);
}
```

```js
// period.test.mjs — the single test for the incoming requirement
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { periodFee } from './period.mjs';

const period = Array.from({ length: 60 }, () => ({
  width: 20, length: 15, height: 10, grams: 3000, zone: 'B1',
}));

test('all shipments in a period past the threshold total at the discounted rate', () => {
  assert.equal(periodFee(period), Math.round(60 * 7490 * 0.88));
});
```

```sh
node --test --test-reporter=tap period.test.mjs \
  | grep -E '^ *(ok|not ok|expected:|actual:|# (tests|pass|fail))'
```

```
not ok 1 - all shipments in a period past the threshold total at the discounted rate
  expected: 395472
  actual: 440410
# tests 1
# pass 0
# fail 1
```

The 44,938-cent gap shows that the extension point generalized along **the wrong
axis**. A rule's signature is `(cents, shipment, config)`: a rule sees a single
shipment. The incoming requirement decides a shipment's fee based on **the size of the
set** instead; by the time the fifty-first shipment is processed, the previous fifty
have already been summed. The extension could only meet this by walking the set
twice, and at that point the caller doing the summing has taken over the whole job —
the extension point serves no purpose.

The generalization had assumed that future change would arrive as **a rule per
shipment**. The change that actually arrived is per period, not per shipment. The same
holds for the configuration layer: none of its six fields covers the discount
threshold or the discount rate.

## Discarding the Generalization

The size of the discarded structure can be counted.

```sh
printf 'lines in the discarded files: %s\n' \
  "$(cat configuration.mjs extension.mjs period.mjs | wc -l | tr -d ' ')"
rm configuration.mjs extension.mjs period.mjs period.test.mjs
```

```
lines in the discarded files: 48
```

In its place, the incoming requirement is met directly: a function that computes the
period fee, one threshold, one rate.

```js
// fee.mjs — generalization discarded, the incoming requirement met directly
import { tier } from './tariff.mjs';

const ZONE_FACTOR = { B1: 1.0, B2: 1.25, B3: 1.6 };
const MINIMUM_CENTS = 6990;
const DISCOUNT_THRESHOLD = 50;
const DISCOUNT_RATE = 0.88;

export function effectiveWeight(s) {
  return Math.max(s.grams, Math.ceil((s.width * s.length * s.height) / 3));
}

export function shipmentFee(s) {
  const withZoneFactor = Math.round(tier(effectiveWeight(s)).cents * ZONE_FACTOR[s.zone]);
  return Math.max(withZoneFactor, MINIMUM_CENTS);
}

export function periodFee(shipments) {
  const total = shipments.reduce((t, s) => t + shipmentFee(s), 0);
  if (shipments.length <= DISCOUNT_THRESHOLD) return total;
  return Math.round(total * DISCOUNT_RATE);
}
```

```js
// fee.test.mjs — the four earlier tests and two for the period discount
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { shipmentFee, periodFee } from './fee.mjs';

const box = { width: 20, length: 15, height: 10 };
const period = (count) => Array.from({ length: count }, () => ({ ...box, grams: 3000, zone: 'B1' }));

test('the tier fee is read from the weight', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B1' }), 7490);
});

test('the zone factor multiplies the base fee', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B3' }), 11984);
});

test('the minimum fee is applied after the zone multiplication', () => {
  assert.equal(shipmentFee({ ...box, grams: 500, zone: 'B2' }), 6990);
});

test('volumetric weight is used when it exceeds actual weight', () => {
  assert.equal(shipmentFee({ width: 40, length: 30, height: 30, grams: 500, zone: 'B2' }), 16125);
});

test('all shipments in a period past the threshold total at the discounted rate', () => {
  assert.equal(periodFee(period(60)), Math.round(60 * 7490 * 0.88));
});

test('a period under the threshold gets no discount', () => {
  assert.equal(periodFee(period(10)), 10 * 7490);
});
```

```sh
node --test --test-reporter=tap fee.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node branches.mjs fee.mjs
node --test --experimental-test-coverage --test-reporter=tap fee.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
```

```
ok 1 - the tier fee is read from the weight
ok 2 - the zone factor multiplies the base fee
ok 3 - the minimum fee is applied after the zone multiplication
ok 4 - volumetric weight is used when it exceeds actual weight
ok 5 - all shipments in a period past the threshold total at the discounted rate
ok 6 - a period under the threshold gets no discount
# tests 6
# pass 6
# fail 0
fee.mjs: 1 decision points
total: 1
# start of coverage report
# -----------------------------------------------------------
# file       | line % | branch % | funcs % | uncovered lines
# -----------------------------------------------------------
# fee.mjs    | 100.00 |   100.00 |  100.00 | 
# tariff.mjs | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# all files  | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# end of coverage report
```

One decision point is left, and it corresponds to a domain rule: whether the period
is under the threshold or not. Branch coverage is 100 percent; there is no path in the
codebase the run does not go through. The fee file imports one module; the previous
version imported three.

| Metric | Generalized | Direct |
|---|---|---|
| Decision points | 8 | 1 |
| Fee file's branch coverage | 45.45% | 100% |
| All files' line coverage | 90.41% | 100% |
| Modules imported | 3 | 1 |
| Meets the incoming requirement | no | yes |

## What the Principle Forbids

This lesson and the previous one measured different things. Keep it simple asks for
the least structure for a requirement that **has** arrived; you are not going to need
it forbids writing structure for a requirement that **has not** arrived. One concerns
the shape of today's work, the other a prediction about tomorrow's.

The principle's boundary lies in the cost of discarding. Here, the 48 discarded lines
stayed inside the library; they affected no external consumer and could be undone with
a single commit in the `git` repository. The same calculation gives a different answer
for a published interface or a persisted data format that requires a backward
migration: there, the cost of a wrong prediction can exceed the cost of not predicting
at all. The question that makes the principle applicable is not "will this be needed
later," it is **"how much more expensive is adding it when needed than adding it
today."** In the fee library, that gap was close to zero: the period discount was
written in six lines, with no extension point at all.

## Summary

- You are not going to need it forbids writing structure today for a requirement that
  has not arrived; the measured quantity is the cost of a wrong prediction.
- The configuration layer and extension point written in today raised the fee file's
  decision-point count from zero to eight and dropped its branch coverage to 45.45
  percent.
- Line coverage at 100 percent does not mean the paths run; unexercised paths show up
  in branch and function coverage.
- The requirement that actually arrived was per period, not per shipment; the
  extension point's signature could not express that, and the single-test run
  finished red with a 44,938-cent gap.
- Discarding the generalization deleted 48 lines; the direct solution met the same
  requirement with one decision point, 100 percent branch coverage, and six tests.
- The principle's criterion is not "will this be needed later," it is how much more
  expensive adding it when needed is than adding it today.

## Next Step

After the generalization is discarded, the remaining file works but is not tidy: the
zone factor table keeps information that belongs to the tariff inside the fee file,
the threshold comparison in the period calculation stays unnamed, and the name `s`
does not say that it stands for a shipment. The next lesson fixes these three flaws
with three mechanical techniques — extract, move, and rename — applies each one
through its intermediate steps, and shows the test run after every step.
