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

# Meaningful Names

Treating naming as a measurable design decision: counting opaque arguments at the call site in a shortened and an intent-revealing version of the same shipment fee calculation, deriving how many candidate words an abbreviation could resolve to, and the relationship between name length and the scope a name lives in.

Completing a language curriculum yields one capability: writing code that produces the
requested behavior. The JavaScript and TypeScript courses delivered that; if the tests
pass, the program is correct. This curriculum opens with a different question. When a
choice must be made between two programs that produce the same behavior, what does the
choice rest on?

The question is unavoidable, because code is not written once and abandoned. A shipment
fee library gets a new zone coefficient six months later, and a volumetric weight divisor
changes a year after that. Whoever makes those changes **reads** the code first. Reading
cost is the actual measure of a design, and a claim like "more readable" does not settle
an argument until tied to a number. This lesson ties naming to two numbers: the number of
arguments at the call site whose meaning cannot be answered, and how many different
domain words an abbreviation could resolve to.

One library runs through the course: shipment fee calculation and routing. Its first
function computes a fee from a shipment's weight, volume, and zone.

## Measure: The Question Answerable at the Call Site

The information a name carries is measured **where it is used**. Someone looking at a
call line asks: which parameter does this argument fill, what does its value mean, what
is the result a fee for? If answering requires opening another file, the name is not
carrying information.

To make this mechanical, call an argument **labeled** if the call text itself states
which parameter it fills — a named field, or a variable name of at least three letters.
Otherwise it is **opaque**: a bare number, a boolean, or a single-letter name. The number
of opaque arguments is a lower bound on the number of questions that cannot be answered
at the call site.

## The Abbreviated Version

The first version works and passes its tests. It computes the volumetric weight, finds
the billable weight, selects the tier, applies the zone coefficient, subtracts the
discount, and does not drop below the minimum fee.

```sh
mkdir -p version-a version-b
```

```js
// version-a/data.mjs — sample shipment and tariff
export const s = { w: 2.4, wd: 30, l: 24, h: 18 };
export const t = { tr: [{ u: 1, r: 38 }, { u: 5, r: 26 }, { u: 10, r: 21 }, { u: Infinity, r: 17 }],
                   zc: { 1: 1, 2: 1.35, 3: 1.8 }, mf: 45 };
```

```js
// version-a/fee.mjs — shipment fee, abbreviated names
export function calc(s, t, z, x) {
  const v = (s.wd * s.l * s.h) / 3000;
  const a = Math.max(s.w, v);
  const k = t.tr.find((i) => a <= i.u);
  const f = k.r * a * t.zc[z];
  return Math.max(f * (1 - x), t.mf);
}
```

```js
// version-a/call.mjs — call site
import { calc } from "./fee.mjs";
import { s, t } from "./data.mjs";
console.log(calc(s, t, 2, 0.12).toFixed(2));
```

None of the four arguments at the call site explains itself. Is `2` a zone code, a tier
index, a quantity? Is `0.12` a discount rate, a tax rate? The answer sits in `fee.mjs`;
whoever is reading has to open a second file.

## The Named Version

The second version performs the same arithmetic. What changes is the names, the
parameter shape, and where the numbers come from.

```js
// version-b/data.mjs — same data, with field names
export const ZONE = { SAME_CITY: 1, NEIGHBORING_ZONE: 2, DISTANT_ZONE: 3 };
export const CONTRACTED_CUSTOMER_DISCOUNT = 0.12;
export const sampleShipment = { weightKg: 2.4, widthCm: 30, lengthCm: 24, heightCm: 18 };
export const domesticTariff = {
  weightTiers: [{ upperLimitKg: 1, ratePerKg: 38 }, { upperLimitKg: 5, ratePerKg: 26 },
                { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: Infinity, ratePerKg: 17 }],
  zoneCoefficients: { 1: 1, 2: 1.35, 3: 1.8 },
  minimumFee: 45,
};
```

```js
// version-b/fee.mjs — same calculation, with intent-revealing names
const VOLUMETRIC_DIVISOR = 3000;

export function calculateShipmentFee({ shipment, tariff, zoneCode, discountRate }) {
  const volumetricWeightKg = (shipment.widthCm * shipment.lengthCm * shipment.heightCm) / VOLUMETRIC_DIVISOR;
  const billableWeightKg = Math.max(shipment.weightKg, volumetricWeightKg);
  const tier = tariff.weightTiers.find((t) => billableWeightKg <= t.upperLimitKg);
  const zonedFee = tier.ratePerKg * billableWeightKg * tariff.zoneCoefficients[zoneCode];
  return Math.max(zonedFee * (1 - discountRate), tariff.minimumFee);
}
```

```js
// version-b/call.mjs — call site
import { calculateShipmentFee } from "./fee.mjs";
import { sampleShipment, domesticTariff, ZONE, CONTRACTED_CUSTOMER_DISCOUNT } from "./data.mjs";
console.log(calculateShipmentFee({ shipment: sampleShipment, tariff: domesticTariff,
  zoneCode: ZONE.NEIGHBORING_ZONE, discountRate: CONTRACTED_CUSTOMER_DISCOUNT }).toFixed(2));
```

Both versions are confirmed to produce the same result.

```sh
node version-a/call.mjs
node version-b/call.mjs
```

```
133.44
133.44
```

The behavior is identical. The difference is only in reading cost, and that cost is
countable.

## Measurement

The tool below counts two things. First, the opaque arguments at a given call site.
Second, taking every name in the implementation that is two letters or shorter, how many
words in the domain vocabulary each one could resolve to — that is, how ambiguous the
abbreviation is.

```js
// name-measure.mjs — counts opaque call-site arguments and how many domain words an abbreviation could mean
import { readFileSync } from "node:fs";

// The library's domain vocabulary: words a name is expected to carry.
const GLOSSARY = ["coefficient", "discount", "divisor", "fee", "kilogram", "limit", "minimum",
  "rate", "shipment", "tariff", "tier", "volume", "volumetric", "weight", "zone"];

// An argument is labeled if the call text itself states which parameter it fills
// (a named field, or a variable name of at least three letters).
const labeled = (arg) => /^[A-Za-z_$][\w$]{2,}\s*:/.test(arg) || /^[A-Za-z_$][\w$]{2,}$/.test(arg);

function countOpaque(file, fnName) {
  const body = readFileSync(file, "utf8");
  const call = body.match(new RegExp(`\\b${fnName}\\(([^()]*)\\)`));
  const args = call[1].replace(/[{}]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
  return { total: args.length, opaque: args.filter((a) => !labeled(a)).length };
}

function abbreviations(file) {
  const body = readFileSync(file, "utf8").replace(/^\/\/.*$/gm, "");
  const names = [...new Set(body.match(/[A-Za-z_$][\w$]*/g))].filter((a) => a.length <= 2);
  return names.map((name) => ({
    name,
    candidates: GLOSSARY.filter((g) => g.startsWith(name[0].toLowerCase())),
  }));
}

for (const [file, fn] of [["version-a/call.mjs", "calc"], ["version-b/call.mjs", "calculateShipmentFee"]]) {
  const r = countOpaque(file, fn);
  console.log(`${file.padEnd(20)} ${fn.padEnd(21)} arguments=${r.total}  opaque=${r.opaque}`);
}

console.log("\nabbreviation  candidate count  candidates");
const short = abbreviations("version-a/fee.mjs");
for (const s of short) console.log(`${s.name.padEnd(9)} ${String(s.candidates.length).padEnd(12)} ${s.candidates.join(", ") || "-"}`);
const unique = short.filter((s) => s.candidates.length === 1).length;
console.log(`\nversion-a abbreviation count = ${short.length}, resolve to one candidate = ${unique}`);
console.log(`version-b abbreviation count = ${abbreviations("version-b/fee.mjs").length}`);
```

```sh
node name-measure.mjs
```

```
version-a/call.mjs   calc                  arguments=4  opaque=4
version-b/call.mjs   calculateShipmentFee  arguments=4  opaque=0

abbreviation  candidate count  candidates
s         1            shipment
t         2            tariff, tier
z         1            zone
x         0            -
v         2            volume, volumetric
wd        1            weight
l         1            limit
h         0            -
a         0            -
w         1            weight
k         1            kilogram
tr        2            tariff, tier
i         0            -
u         0            -
f         1            fee
r         1            rate
zc        1            zone
mf        1            minimum

version-a abbreviation count = 18, resolve to one candidate = 10
version-b abbreviation count = 1
```

Four opaque arguments dropped to zero. The abbreviation table gives the second result: of
eighteen short names in the abbreviated implementation, ten resolve to exactly one
glossary word, five resolve to none, and three resolve to more than one. Ambiguity is not
the only failure mode. `w` genuinely stands for weight and resolves to `weight`; `wd`
stands for width but resolves to that same single candidate, `weight` — a confident,
wrong answer. `k` stands for the selected tier and resolves only to `kilogram`, another
confident, wrong answer. A single matching candidate is not proof the name is right.

The named version keeps exactly one short name: the `t` parameter passed to the `find`
callback. That leftover is not a coincidence; it is the subject of the next section.

## Name Length Depends on Scope

A short name is not a defect by itself. The measure is the scope the name **lives in**.
The difference between the `t` inside a one-line callback and a module-level export named
`t` is the distance a reader travels to find the definition: on the same line in the
first case, in another file in the second.

A practical rule follows: a name's length should grow with the amount of code it stays
visible across. A loop counter `i` is defensible inside a three-line loop, not as an
exported constant. The reasoning runs in reverse too: an unnecessarily long name in a
narrow scope (`currentShipmentVolumetricWeightValue`) lengthens the line without adding
information.

## What a Name Should Convey

The measurement says how much is missing; it does not say which information needs
adding. Four criteria do that work.

**Units and type live inside the name.** The difference between `weightKg` and `weight`
is that a mistake mixing grams with kilograms becomes visible before the code compiles.
The suffixes on `widthCm`, `upperLimitKg`, and `discountRate` do this work;
`discountRate` and `discountAmount` cannot substitute for each other.

**A name must not lie.** A variable named `zoneList` that actually holds a mapping is
worse than no name at all: whoever reads it proceeds on a false assumption. The cost of a
wrong name is every conclusion drawn before the mistake is found.

**One word per concept.** If `getTariff`, `fetchTariff`, and `retrieveTariff` all appear
in the same library, the reader assumes three different operations. The vocabulary stays
fixed throughout the course: a shipment's billable weight appears everywhere as
`billableWeightKg`.

**Searchability.** The number `3000` appears in hundreds of places across a codebase;
`VOLUMETRIC_DIVISOR` appears once. When the number needs to change, the name is what gets
searched for. Leaving a bare number unnamed costs not only reading, but changing the
code.

## Summary

- Naming is not a stylistic preference but a measurable decision: the measure is the
  opaque argument count, a lower bound on the questions a call site cannot answer.
- In the abbreviated version, all four arguments were opaque; in the named version, none
  were. Both versions produce the same result (133.44); the difference is only in reading
  cost.
- The abbreviation-resolution table gave the second measure: of eighteen short names, ten
  resolve to exactly one domain word, five to none, and three to more than one — and a
  single matching candidate is not proof the name is right.
- Name length grows with the scope a name lives in; a short name inside a one-line
  callback and an exported short name are not the same thing.
- A good name carries a unit and a type, does not lie, uses one word per concept, and is
  searchable; naming a bare number reduces the cost of changing code, not just reading it.

## Next Step

One thing stands out in the named version: the `VOLUMETRIC_DIVISOR` constant needed no
comment line, because its name already says what it is. In the abbreviated version, the
same information could only be given through a comment. Does this observation
generalize? Is the presence of a comment a sign that the code had to say something it
could not say itself? The next lesson answers this by measurement: how much of a comment
can be translated into a name, which comments cannot be, and at what point a comment
drifting apart from the code turns into a silent error.
