---
title: Consistency
source: 'https://academia.sh/en/courses/clean-code/consistency'
course: 'Clean Code'
language: en
updated: '2026-08-23T07:01:11+00:00'
license: 'CC BY-SA 4.0'
---

# Consistency

Measuring whether the same job is done the same way throughout a project: a scan that finds the number of forms in a package that does three separate jobs in three separate ways, repeating the measurement after unification, and counting the changes that merging forms causes in outputs and error behavior.

In the actor layout, every decision got an owner. Something still nags in the library: in
one file, an amount is written with `toFixed`, in another it is rounded by hand with the
currency prepended; in one place a not-found case is reported by throwing an error, in
another by returning a null value.

None of this is wrong. All three work, all three pass their tests. They are merely
**different**, and the cost of that difference accumulates on the reader: the answer to
the same question is searched for anew in every file. This lesson ties the difference to
a number — how many separate forms the same job takes.

## The Criterion: Formats per Job

A codebase has jobs that repeat: writing an amount of money, reporting a not-found case,
exposing a module. For each job, the number of distinct writing forms used in that
codebase is the measure of consistency. The target is always one; which form it is comes
second.

The number corresponds directly to a cost. If a job is done in three forms, someone who
needs that job in a new place sees three examples and has to decide which one to follow.
When making that decision, they usually take whichever file they last looked at as the
example; the form count does not shrink, the distribution just becomes random.

## Three Jobs, Three Separate Forms

The three modules in the package do three separate jobs: the fee line, the label line,
the daily total. All three write an amount, all three report a not-found case, all three
expose something.

```sh
mkdir -p mixed unified
```

```js
// mixed/fee.mjs — fee line
export function feeLine(shipment, tariff) {
  const tier = tariff.tiers.find((t) => shipment.weightKg <= t.maxKg);
  if (tier === undefined) throw new RangeError("tier not found");
  return `${shipment.code} ${(tier.ratePerKg * shipment.weightKg).toFixed(2)} TL`;
}
```

```js
// mixed/label.mjs — label line
export const labelLine = (shipment, zoneNames) => {
  const zone = zoneNames[shipment.zoneCode];
  if (zone === undefined) return null;
  return `${shipment.code} ${zone} ${shipment.declaredValue.toFixed(2)}TL`;
};
```

```js
// mixed/report.mjs — daily total
function dailyTotal(shipments, day) {
  const dayShipments = shipments.filter((s) => s.day === day);
  if (dayShipments.length === 0) return { error: "no shipment for that day" };
  const total = Math.round(dayShipments.reduce((t, s) => t + s.fee, 0) * 100) / 100;
  return `day ${day} TL ${total}`;
}

export { dailyTotal };
```

The three files look as if they were written independently of each other, and that may
well be the case: each one was written at a different time, by a different person,
following that day's habit. The result is three separate writing conventions inside the
same package.

## The Unified Version

In the second version, a single form was chosen for each job: the amount is written
with `toFixed(2)` and the currency is appended with a space, a not-found case is reported
by throwing a domain error, and the export is done at the top of the declaration.

```js
// unified/fee.mjs — fee line
export function feeLine(shipment, tariff) {
  const tier = tariff.tiers.find((t) => shipment.weightKg <= t.maxKg);
  if (tier === undefined) throw new RangeError("tier not found");
  return `${shipment.code} ${(tier.ratePerKg * shipment.weightKg).toFixed(2)} TL`;
}
```

```js
// unified/label.mjs — label line
export function labelLine(shipment, zoneNames) {
  const zone = zoneNames[shipment.zoneCode];
  if (zone === undefined) throw new RangeError("zone not found");
  return `${shipment.code} ${zone} ${shipment.declaredValue.toFixed(2)} TL`;
}
```

```js
// unified/report.mjs — daily total
export function dailyTotal(shipments, day) {
  const dayShipments = shipments.filter((s) => s.day === day);
  if (dayShipments.length === 0) throw new RangeError("no shipment for that day");
  return `day ${day} ${dayShipments.reduce((t, s) => t + s.fee, 0).toFixed(2)} TL`;
}
```

## The Form Scanner

The scanner carries the patterns of the forms to recognize for each job, finds which
forms appear in the files of a directory, and counts how many separate forms are used
per job.

```js
// form-scan.mjs — counts how many separate forms the same job takes
import { readFileSync, readdirSync } from "node:fs";

const JOBS = {
  "money format": [["toFixed + space + TL", /toFixed\(2\)\} TL/], ["toFixed + adjoining TL", /toFixed\(2\)\}TL/],
    ["Math.round + TL prefix", /Math\.round\(/]],
  "not-found handling": [["throwing an error", /throw new \w*Error/], ["null value", /return null/],
    ["error object", /return \{ error:/]],
  "exporting": [["export function", /^export function /m], ["export const arrow", /^export const \w+ = \(/m],
    ["export block at the end", /^export \{/m]],
};

for (const dir of process.argv.slice(2)) {
  const files = readdirSync(dir).filter((a) => a.endsWith(".mjs")).sort();
  const text = files.map((a) => [a, readFileSync(`${dir}/${a}`, "utf8")]);
  console.log(dir);
  let total = 0;
  for (const [job, forms] of Object.entries(JOBS)) {
    const found = forms
      .map(([name, pattern]) => [name, text.filter(([, m]) => pattern.test(m)).map(([f]) => f)])
      .filter(([, files]) => files.length > 0);
    total += found.length;
    console.log(`  ${job.padEnd(20)} forms=${found.length}`);
    for (const [name, f] of found) console.log(`    ${name.padEnd(26)} ${f.join(", ")}`);
  }
  console.log(`  total forms=${total}  (1 per job is the target, target=${Object.keys(JOBS).length})`);
}
```

```sh
node form-scan.mjs mixed unified
```

```
mixed
  money format         forms=3
    toFixed + space + TL       fee.mjs
    toFixed + adjoining TL     label.mjs
    Math.round + TL prefix     report.mjs
  not-found handling   forms=3
    throwing an error          fee.mjs
    null value                 label.mjs
    error object               report.mjs
  exporting            forms=3
    export function            fee.mjs
    export const arrow         label.mjs
    export block at the end    report.mjs
  total forms=9  (1 per job is the target, target=3)
unified
  money format         forms=1
    toFixed + space + TL       fee.mjs, label.mjs, report.mjs
  not-found handling   forms=1
    throwing an error          fee.mjs, label.mjs, report.mjs
  exporting            forms=1
    export function            fee.mjs, label.mjs, report.mjs
  total forms=3  (1 per job is the target, target=3)
```

The total form count dropped from nine to three, and three equals the number of jobs —
meaning every job is done in a single form. The file lists also flipped: before, every
form had a single file; now, the single form has three files.

## The Cost of Unification

Merging forms is not a free operation. Merging the money format changes the output
text; merging the not-found form changes the error behavior. The changes need to be
counted.

```js
// compare.mjs — the effect of unification on outputs and not-found behavior
import { feeLine as mFee } from "./mixed/fee.mjs";
import { labelLine as mLabel } from "./mixed/label.mjs";
import { dailyTotal as mTotal } from "./mixed/report.mjs";
import { feeLine as uFee } from "./unified/fee.mjs";
import { labelLine as uLabel } from "./unified/label.mjs";
import { dailyTotal as uTotal } from "./unified/report.mjs";

const TARIFF = { tiers: [{ maxKg: 5, ratePerKg: 26 }, { maxKg: 30, ratePerKg: 17 }] };
const REGIONS = { 1: "Same City", 2: "Neighboring Zone" };
const SHIPMENT = { code: "GN-4172", weightKg: 2.4, zoneCode: 2, declaredValue: 1500, day: 3, fee: 62.4 };
const LIST = [SHIPMENT, { ...SHIPMENT, code: "GN-4173", fee: 51.2 }];

const attempt = (fn) => {
  try {
    const result = fn();
    return typeof result === "object" && result !== null ? JSON.stringify(result) : String(result);
  } catch (error) {
    return `${error.constructor.name}: ${error.message}`;
  }
};

const CASES = [
  ["fee", () => mFee(SHIPMENT, TARIFF), () => uFee(SHIPMENT, TARIFF)],
  ["label", () => mLabel(SHIPMENT, REGIONS), () => uLabel(SHIPMENT, REGIONS)],
  ["total", () => mTotal(LIST, 3), () => uTotal(LIST, 3)],
  ["label/missing", () => mLabel({ ...SHIPMENT, zoneCode: 9 }, REGIONS),
    () => uLabel({ ...SHIPMENT, zoneCode: 9 }, REGIONS)],
  ["total/missing", () => mTotal(LIST, 7), () => uTotal(LIST, 7)],
];

for (const [label, mixed, unified] of CASES) {
  const a = attempt(mixed), b = attempt(unified);
  console.log(`${label.padEnd(14)} ${a === b ? "same   " : "changed"} ${a.padEnd(34)} -> ${b}`);
}
```

```
fee            same    GN-4172 62.40 TL                   -> GN-4172 62.40 TL
label          changed GN-4172 Neighboring Zone 1500.00TL -> GN-4172 Neighboring Zone 1500.00 TL
total          changed day 3 TL 113.6                     -> day 3 113.60 TL
label/missing  changed null                               -> RangeError: zone not found
total/missing  changed {"error":"no shipment for that day"} -> RangeError: no shipment for that day
```

Four of the five cases changed. Three are presentation decisions: the currency suffix,
the number of decimal places, and the ordering are now the same everywhere. One is a
behavior decision: two functions now throw an error instead of returning a null value or
an error object, and every place that calls them must be updated accordingly.

This is the real cost of consistency work. The measurement mechanizes the task but does
not make the decision; the scanner says how many forms there are, not which one becomes
the standard. The line `day 3 TL 113.6` becoming `day 3 113.60 TL` is a choice, and once
made, it cannot be taken back on its own.

## Choosing the Standard

Three criteria are useful for deciding which one becomes the standard.

**Prevalence.** The form already most used in the codebase is the one that can be
standardized with the fewest changes. The scanner's file list gives this information
directly.

**Alignment with the outward-facing surface.** If a form is visible to a user or
another system, that expectation comes before internal preference. If the money format
appears in invoice output, what determines the standard is accounting's format.

**Resistance to misuse.** In the not-found example, throwing an error is more resistant
than returning a null value; one of the earlier lessons measured this — a null value
carries the error far from where it was produced.

None of the criteria involve "which looks nicer." Where a consistency discussion gets
stuck is comparing forms aesthetically; the real gain, however, comes from the form
being **single**, not from which one it is. Once the decision is made, the task
mechanizes: the scanner is run on the codebase, and every job with a form count greater
than one is listed.

Something is still missing in the unified version. Three files write the same money
format three times; the form is single, but the writing is repeated in three places.
These are separate problems: one is the same job being done in different forms, the
other is the same information sitting in more than one place.

## Summary

- The measure of consistency is how many separate forms a job takes in a codebase; the
  target is always one, and which form it is comes second.
- In the three-module package, the money format, the not-found report, and the export
  each had three forms; the total of nine forms dropped to three, matching the number
  of jobs.
- Unification is not free: four of five cases changed; three were presentation
  decisions, one was an error-behavior decision, and it concerns every call site.
- Choosing the standard uses the criteria of prevalence, alignment with the
  outward-facing surface, and resistance to misuse; aesthetic comparison does not end the
  discussion.
- The form being single and that form being written in a single place are separate
  problems; the second one is still there after unification.

## Next Step

The decisions in this topic were all **local**: what a name says, what a signature
accepts, what a module exposes, whom a file serves, in what form a job gets done. Each
one could be given by looking at a single file and checked by looking at a single file.

As a codebase grows, a cost appears that does not fit this scale. The same information
starts sitting in more than one place, and when one copy is updated, the others fall
behind. Flexibility not needed today gets built in today and becomes a load added onto
every change until the day it turns out not to be needed. Changing one decision spreads
not to a single file but to a set of places whose size is not known in advance. The next
topic addresses these three costs, starting by telling apart the kinds of duplication:
the same information sitting in two places is not the same thing as two pieces of code
that merely happen to look alike.
