Naming
A name is the interface every future reader uses instead of the body. It has to carry domain meaning, role, units and whether calling it changes anything.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
What does a name have to carry so that a reader can use the thing correctly without reading its implementation?
A new engineer asks in review what process(data, flag) does. Three people who have worked on the file all answer, they do not agree, and two of them are wrong.
Names are style. Pick something reasonable, keep it short, keep the diff small, and move on — the implementation is right there if anyone needs the details.
The implementation is right there for the author, this week. Later, "reading the body" means reading the four things it calls, and a name that answered the question would have saved all of it.
- The implementation is right there for the author, this week. Later, "reading the body" means reading the four things it calls, and a name that answered the question would have saved all of it.
- Generic names attract unrelated behaviour as requirements arrive. Nothing about
processsays it must not also send an email, so eventually it does, and the name still fits — which is exactly the problem (Single Responsibility, Carefully). - A name that was accurate becomes a lie when the behaviour changes underneath it.
validate()grows an audit write; nobody renames it; every caller now performs a write it does not know about, and the callers that retry now write twice. - Two teams introduce
Customerfor two different concepts — the billing entity and the logged-in person — and every conversation, ticket and bug report from then on needs a disambiguating sentence (Ubiquitous Language).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Renaming is minutes inside a module with a compiler and a working IDE; it is a two-deploy migration once the name is a database column, an event type or a URL segment.
- The vocabulary has to work for people who did not sit in the meeting where the concept was invented, and for the author six months later, who is effectively one of them.
- Thirty thousand lines already use the current words. A better vocabulary that applies only to new code makes the codebase worse before it makes it better, because now there are two.
- A name must never contradict what the code does. A vague name gets read; a confidently wrong one does not, which is why it is the more expensive failure.
- One concept, one name, everywhere. If two names exist for one thing, a reader will eventually assume they are two things — and design accordingly.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- The author owns naming the concept, not describing the mechanism: what this is in the problem domain, not how it currently happens to work.
- Review owns catching the name that no longer matches the behaviour. It is the only point in the process where someone reads the code without already knowing what it is supposed to say (Review as Design Feedback — and Why It Arrives Too Late).
- The business owns the vocabulary. Engineering does not get to coin a word for something the domain already has a word for, and when it does, the translation cost is paid in every future conversation.
- A module's exported names are part of its interface; its internal names are not. Inside the boundary a rename is free, and across it a rename costs every caller (Designing a Module Interface).
- Names that reach persisted rows, event payloads or public URLs have left the codebase and become compatibility obligations. Treat that crossing as deliberate (Backward Compatibility as a Constraint).
- Scope sets the budget.
iin a three-line loop is a better name thancurrentIndexIntoTheCollection; a module-level export gets the long specific name because its readers arrive without context.
What a reader is asking the name
A reader arriving at a call site has a specific list of questions, and they will answer them either from the name or by opening the body. Every question the name answers is a body they do not have to read — and, more importantly, a body they do not have to read *correctly*.
The list is short and it is always the same. What is this in the problem domain, what role does this argument play, what unit is this number in, does calling it change anything, and can it fail. A name that answers four of five is doing most of the work.
- The unit and the failure mode are the two most often left out, and they are the two the type system can carry for free (Units in Names and Types, Result Types).
- A name cannot answer "why". That is what comments are for, and pretending otherwise is how the "self-documenting code" argument overreaches (Comments).
| The reader's question | A name that answers it | A name that does not |
|---|---|---|
| What is this, in the business? | chargeback, settlementBatch, gracePeriodEndsAt | item, record, data2 |
| What role does this argument play? | transfer(from: Account, to: Account) | transfer(a: Account, b: Account) |
| What unit is this number in? | timeoutMs, weightKg, amountMinorUnits | timeout, weight, amount |
| Does calling this change anything? | findUser vs createUserIfMissing | getUser, which does both |
| Can it fail, and how? | parseEmail(): Result<Email, ParseError> | getEmail(), which throws sometimes |
The name that lies
The worst name in a codebase is not the vague one. It is the one that confidently describes something the code no longer does, because a vague name sends a reader to the implementation and a confident one sends them straight past it.
This happens almost entirely by accretion. Nobody writes a function called validateOrder that writes to the database. Someone adds an audit requirement, the check is already there, the write goes next to it, and the name is not part of the diff.
1// Reads like a check. Is not.2function validateOrder(order: Order): boolean {3 const ok = rules.every((r) => r.check(order))4 auditLog.write({ orderId: order.id, ok }) // a write5 order.validatedAt = new Date() // a mutation6 return ok7}8 9// Two callers wrapped this in a retry, because "it is just a check".10// Each retry wrote another audit row and moved validatedAt.11 12// The split the name should have forced:13function checkOrder(order: Order): CheckResult { /* pure */ }14function recordOrderCheck(order: Order, r: CheckResult): void { /* writes */ }Notice that the fix is not a better name for the existing function — it is a different decomposition that the naming problem revealed. When a name cannot be made honest without a very long name, the unit is doing two things (Side Effects).
What an ambiguous name costs when the domain gets more precise
The interesting cost of a name is not the confusion today. It is what happens when the business discovers that the thing you named once is actually two things — which is the single most common way a domain evolves.
A ledger has a balance. Then someone asks for pending card authorisations to be held back, and now there are two balances: what you have and what you can spend. Every existing use of balance has to be classified, and the name gives you no help at all in doing it.
The balance a customer may spend now excludes pending card authorisations, while statements, interest and reconciliation must keep using the full ledger balance.
The compiler flags nothing, because the type did not change. Each of the eight sites has to be read and a judgement made about which balance the author meant — including the sites where the author never considered the question. The classification is the work; the edit is trivial.
Only the sites that genuinely mean "spendable" change. The others were already saying which one they meant, so they are not in the diff and do not need to be read.
git blame across eight files, and if the two balances had turned out to be one concept after all, the split would be extra vocabulary buying nothing.How to build it
Most important first.
- Name the concept, not the mechanism.
RetryingHttpOrderFetchernames how it works today;OrderSourcenames what it is for, and survives the day the transport changes. - Put the role in the name.
userin a function that takes two of them tells a reader nothing;actorandsubjecttell them who is doing what to whom. - Carry the unit, the currency and the timezone — in the name at minimum, in the type where it is worth it (Units in Names and Types).
- Say whether it changes anything. Queries read like nouns or questions and return; commands read like imperatives and act. A query that mutates should be renamed until the mutation is visible in the call site (Side Effects).
- Prefer the domain word to the technical word wherever both exist:
chargeback, notnegativeTransaction(Naming and Domain Language). - When you cannot name it, that is information. A unit you cannot name usually does more than one thing, and the naming difficulty is the cheapest signal you will get (Designing by Responsibility).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Under names that carry meaning, the next change costs a search for a domain term and lands in the places that own it. Under
process/data/Manager, the same change starts with a discovery phase — open each candidate, read the body, decide whether this is the one — and on a mature codebase that phase is most of the estimate. - A rename contained in one module costs minutes with a compiler and a test run. The same rename after the name reached a column, a Kafka topic or a public URL costs an expand-and-contract migration, two deploys and a backfill (Expand and Contract).
- The change after that gets worse in a specific way: every new caller of a badly named thing adds a fixed "check what this actually does" cost, so the price of the ambiguity grows with fan-in even in weeks when nobody edits the code (Fan-in and Fan-out).
- Precise names are longer. They cost line width, wrap awkwardly, and make a dense expression harder to scan — a real cost that "just use descriptive names" ignores.
- Insisting on domain vocabulary slows down engineers who do not yet know the domain, and guarantees a transitional period where two vocabularies coexist and both are half-right.
- Renaming is churn. It invalidates
git blame, conflicts with every open branch, and produces diffs that reviewers skim — so a rename can import a bug precisely because it looked mechanical.
What can go wrong
- A codebase-wide rename in one commit, colliding with every open branch and producing a diff no one can review — so it gets approved unread, which is where the real bug enters.
- The name is improved and the concept is not:
databecomescustomerData, which is the same nothing with more syllables. - Type information is encoded into names the compiler already checks (
strName,listOfOrders), so the name goes stale the moment the type changes and now actively misleads. - The team agrees a glossary, puts it in a wiki, and never enforces it. Six months later it is documentation that is wrong, which is worse than no glossary (Documentation Decay).
- The codebase depends on the business vocabulary, in that direction. When the business renames "Client" to "Member", the code inherits either a rename or a permanent translation nobody wrote down.
- Every caller depends on a name meaning what it says. That dependency is invisible and uncheckable, which is why a misleading name is a coupling you cannot lint for.
- Names in stored data and wire formats create dependents outside your repository, including consumers you cannot enumerate (One Vocabulary: Naming and Consistency).
- "Good names mean comments are unnecessary." Some context has no name: why the threshold is 4.5 seconds, which regulation forces this ordering, which upstream bug this works around. That belongs in a comment (Comments).
- "Short names are bad." Scope decides. A one-letter name with a three-line lifetime is fine and often clearer; the same name as a module export is not.
- "Rename everything now." A big-bang vocabulary sweep is a large unreviewable diff at the worst possible moment. One module at a time, with the old name aliased during the transition, is slower and much cheaper (Incremental Migration).
- "The compiler does not care, so this is cosmetic." The compiler is not the audience. The audience is the person who has forty minutes to make a change in code they have never seen, which is the situation this whole domain is about.
- utility-dumping-ground
- primitive-obsession
Testing it, and how it ages
- Test names are names, and they are read more often than the tests are run.
it("works")costs a reader the whole body to learn what broke;it("refuses a refund after the dispute window closes")costs nothing. - A test that needs a comment explaining what the function under test does is reporting a naming failure in the production code, not in the test (Testing as Design Feedback).
- Nothing automated tests a name. Compilers check types, linters check shape, and only a human notices that
validatewrites. Budget review attention accordingly.
- Vocabulary drifts as the business learns what it actually sells. Plan for renames as ordinary maintenance rather than treating each one as an admission of an earlier mistake.
- Names that describe a domain concept outlive names that describe a mechanism — unless the mechanism genuinely is the concept, which for an
SmtpClientit is. The rule is not "never name the technology". - Once a name is persisted it stops evolving with the code. Expect a permanent gap between the internal name and the stored one, and put the translation in exactly one place (Anti-Corruption Layer).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat a reader uses the name instead of the body follows from how reading works, so it holds across languages, paradigms and decades — the specific conventions differ, the obligation does not.
- LANGUAGE-SPECIFICIn a statically typed language with real tooling a rename is a mechanical refactor the compiler verifies; in Python, Ruby or JavaScript the same rename is a text search that misses dynamic dispatch, string keys and reflection, so bad names are meaningfully more expensive to fix there and worth more effort up front.
- SCALE-SPECIFICAt three engineers who talk daily, a shared but undocumented vocabulary genuinely works and the name matters less. At fifty across time zones the name is the entire communication channel, and the same imprecision that was harmless becomes the reason two modules disagree about what a "subscription" is.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Programming Languages & Runtime Internals — whether a rename is a verified refactor or a text search depends on whether the language resolves symbols statically, which is why the same advice costs different amounts in different languages.