NamingGENERALLANGUAGE-SPECIFICSCALE-SPECIFIC

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.

The question

What does a name have to carry so that a reader can use the thing correctly without reading its implementation?

The requirement

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.

The obvious build

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.

Why it breaks

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.

How it breaks as requirements change
  • 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 process says 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 Customer for 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).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

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.

Constraints
  • 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.
Invariants
  • 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.

Responsibilities
  • 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.
Boundaries
  • 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. i in a three-line loop is a better name than currentIndexIntoTheCollection; 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 questionA name that answers itA name that does not
What is this, in the business?chargeback, settlementBatch, gracePeriodEndsAtitem, 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, amountMinorUnitstimeout, weight, amount
Does calling this change anything?findUser vs createUserIfMissinggetUser, which does both
Can it fail, and how?parseEmail(): Result<Email, ParseError>getEmail(), which throws sometimes

The name that lies

PARADIGM-SPECIFICThe command/query split is sharpest in imperative and OO code where a method can quietly mutate its receiver. In a language where effects are tracked in the type — Haskell's IO, or a codebase disciplined about returning new values — the signature already tells the reader, and the naming convention is doing less of the work.

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.

A query name over a command
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 write
5 order.validatedAt = new Date() // a mutation
6 return ok
7}
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.

Pending authorisations must be held back from spendable funds
The change

The balance a customer may spend now excludes pending card authorisations, while statements, interest and reconciliation must keep using the full ledger balance.

One field, `Account.balance`, used everywhere it seemed to fit
AccountTransferServiceStatementPdfInterestJobMobileApiSerializerFraudRulesReconciliationExportSupportAdmin
teststransfer_teststatement_testinterest_testmobile_api_testfraud_testreconciliation_test
8 modules · 6 test files

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.

Two named fields, `ledgerBalance` and `availableBalance`, with no bare `balance` anywhere
AccountTransferServiceMobileApiSerializerFraudRules
testsaccount_balance_testtransfer_testmobile_api_test
4 modules · 3 test files

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.

what it cost Two concepts where callers previously had one: every reader now has to learn a domain distinction, every new endpoint has to choose, and someone will choose wrong in a way that is silent. The rename itself churned 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. RetryingHttpOrderFetcher names how it works today; OrderSource names what it is for, and survives the day the transport changes.
  • Put the role in the name. user in a function that takes two of them tells a reader nothing; actor and subject tell 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, not negativeTransaction (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.

Cost of the next change
  • 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).
What the recommended approach costs
  • 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

Failure modes
  • 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: data becomes customerData, 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).
Dependencies, and their direction
  • 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).
Misreads
  • "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.
Smells this explains
  • utility-dumping-ground
  • primitive-obsession

Testing it, and how it ages

What to test, and at which boundary
  • 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 validate writes. Budget review attention accordingly.
How this design ages
  • 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 SmtpClient it 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.

Domains that do not exist yet
  • 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.