NamingDOMAIN-SPECIFICGENERALCONTESTED

Naming and Domain Language

When code uses the words the business uses, a reader can apply domain knowledge instead of tracing execution. That is the difference between reading and deducing.

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 reader gain when the code speaks the same language as the people who asked for it?

The requirement

Support reports that "the grace period is not applying to annual plans". An engineer greps for grace, finds nothing, and spends two hours discovering it is implemented as status === 2 && expiredAt + config.buffer > now.

The obvious build

Use clear technical names. status, flag, type, record are precise, they are universally understood, and they do not depend on business jargon that changes every reorg.

Why it breaks

They are universally understood and locally meaningless. status === 2 requires knowing what 2 means, which requires finding the enum, which requires knowing what it is called.

How it breaks as requirements change
  • They are universally understood and locally meaningless. status === 2 requires knowing what 2 means, which requires finding the enum, which requires knowing what it is called.
  • A bug report arrives in domain words and there is no path from those words to the code. The grep fails, and the translation lives in the head of whoever has been there longest (Bus Factor).
  • As the domain gets more precise, technical names cannot follow. When the business splits "cancellation" into "voluntary churn" and "involuntary churn", a field called cancelled has nowhere to put the distinction, so it becomes cancelled plus a second flag (Boolean Flag Explosion).
  • The reader cannot use what they know. Someone who understands subscriptions can predict what subscription.pause(reason) does and can spot that pause on an already-past-due subscription is suspicious. Nobody can predict anything about updateStatus(2).
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
  • The business vocabulary is not always clean: "order" means the shopping cart to the front-end team, the fulfilment record to the warehouse and the invoice line to finance, and all three are in the same repository.
  • Some domain terms are legally or contractually defined and cannot be improved on, however awkward they are.
  • The existing code uses a different vocabulary, and every translation between the two is a place a misunderstanding can enter.
Invariants
  • One word, one meaning, within a boundary. If a word means two things, either the boundary is wrong or the word is (Ubiquitous Language).
  • A term used in a ticket, a conversation and the code must refer to the same thing, or the code is not searchable from the outside.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The domain expert owns the words. Engineering's job is to notice when the same word is being used for two things and to make somebody decide which is which.
  • The code owns being greppable from a support ticket: the words in the ticket should find the code without a translation step.
  • Each bounded context owns its own vocabulary, and the adapter between contexts owns the translation — one place, explicit, tested (Anti-Corruption Layer).
Boundaries
  • A vocabulary boundary is a context boundary. Where two parts of the business genuinely use a word differently, that is a seam, and pretending it is one word produces a model that serves neither (Aggregates).
  • Inside a context, the language is uniform and translation is forbidden. At the edge, translation is mandatory and lives in one adapter.
  • Persisted names and external contracts are a second boundary: they encode the vocabulary as it was on the day they were created, which is why they drift from the internal language and need an explicit map (Versioned Interfaces).

From a bug report to the line of code

The concrete benefit is a path. A report arrives in the language of the business, and the question is how many translation steps stand between those words and the code that implements them. Every step is a place the trail can go cold, and steps that live in a person's head go cold permanently.

This is why domain naming is not an aesthetic preference: it is what makes the codebase searchable from the outside, by people who are not going to read it.

  • The lower path costs a person's time twice: theirs and yours, and it is unavailable at 3am.
  • It is also a single point of failure with notice period attached (Knowledge Sharing).
  • The upper path degrades gracefully: even a partly-domain-named codebase gives the grep somewhere to land.
Two routes from "the grace period is not applying"
domain names: one hoptechnical names: no hita mapping that exists in one headSupport ticket: "grace period"grep "grace"GracePeriod.applies()Ask whoever knowsstatus===2 && expiredAt+bufferThe change
UserLLMAgentToolDataDecisionHumanGuardrail

What the reader can deduce

SCALE-SPECIFICOn a two-person team where both people were in the meeting, technical naming genuinely costs little, because the mapping is in both heads and stays fresh. The economics flip as soon as there is someone who was not there — a new hire, a support engineer, an on-call from another team — which typically happens well before anyone notices it has happened.

The deeper benefit is that domain names let a reader use knowledge they already have instead of executing the code in their head. A person who understands subscriptions knows that pausing an already-cancelled one is probably wrong, before reading a single line of the implementation.

Technical names remove that ability entirely. Nothing about updateStatus(2) can be checked against domain knowledge, so every review of it is a review of the mechanism only.

The same operation, and what a reviewer can catch
Technical vocabulary
subscription.updateStatus(2, { until: d })

// A reviewer can check: does 2 exist in the enum?
// Is d in the future?
// They cannot check anything about the business.
Domain vocabulary
subscription.pause(PauseReason.PaymentFailure, until: d)

// A reviewer who knows the domain can ask:
// can a past-due subscription be paused at all?
// does pausing stop the dunning schedule?
// does the paid-through date move?

The second version admits domain review. The three questions it invites are exactly the ones that produce production incidents when nobody asks them, and they are askable by a product person who cannot read the implementation — which more than doubles the number of people who can catch the error.

Where domain naming goes wrong

The failures are specific and mostly come from adopting the vocabulary uncritically rather than from adopting it at all.

Vocabulary failures and their responses
TriggerSymptomCauseResponse
Two teams use "order" for different thingsA model with fields that are meaningless for half its instancesOne vocabulary forced across two contextsMake the boundary explicit and translate at the edge — SalesOrder and FulfilmentOrder (Anti-Corruption Layer)
The business itself uses a word looselyTwo engineers implement two meanings and both pass reviewAmbiguity imported with the term and hidden by precise-looking codeTake it back to the domain expert and make them choose; do not resolve it silently in a commit
A product rename: "Pro" becomes "Team"New code says Team, old code says Pro, both are liveThe rename was never scheduled as workRename inside the boundary, map at the persisted edge, and delete the old name on a date (Expand and Contract)
A glossary exists and disagrees with the codeNewcomers learn the wrong meaning confidentlyThe glossary is not in the same commit as the code it describesMove it next to the code and delete anything you will not maintain (Docs Close to Code)
Infrastructure classes given domain namesOrderChannel turns out to be a connection poolDomain naming applied where there is no domainName technical things technically; the rule is about the domain layer, not about every file (Architecture Boundaries)

How to build it

Most important first.

  • Take the words from the conversation, not from the schema. If the meeting says "dunning", the class is Dunning — a word engineering would never have invented is exactly the word worth keeping (Ubiquitous Language).
  • Make states named domain concepts rather than integers or booleans: PastDue, InGracePeriod, Cancelled — each of which a domain expert can confirm or correct without reading code (State Machines).
  • When the business word is ambiguous, do not resolve it silently in code. Take the ambiguity back to the business, because two engineers guessing differently is how one system ends up with two meanings of "order".
  • Name operations after what the business does, not what the storage does: renewSubscription, not updateSubscriptionRow. The second is true and useless.
  • Keep a short glossary next to the code rather than in a wiki, so the words and their definitions move in the same commit (Docs Close to Code).
  • Rename as the business learns. A vocabulary change is a real, small maintenance cost, and paying it keeps the searchability the whole approach depends on (Rename).

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 domain naming, a change described in business words has a search path: the words in the request appear in the code, so discovery is a grep rather than an investigation. On a mature codebase that is usually the largest single line item in the estimate.
  • Under technical naming, every change starts by finding the person who knows the mapping. That cost does not decrease as the codebase matures — it increases with the number of concepts, and it disappears entirely when that person leaves, along with the mapping.
  • The next change to the vocabulary itself costs a mechanical rename inside the boundary and a compatibility shim outside it. Budget for it; the alternative is a permanent divergence between what the business says and what the code says (Expand and Contract).
What the recommended approach costs
  • Domain vocabulary is harder for newcomers, who must learn the business before they can read the code. That onboarding cost is real and front-loaded, and it is paid by every hire.
  • Renaming with the business is ongoing maintenance with no visible feature output, which makes it hard to prioritise and easy to defer indefinitely.
  • Committing to a domain word can entrench a bad one. Some business terms are genuinely muddled, and adopting them faithfully imports the muddle into the type system.

What can go wrong

Failure modes
  • Domain words are adopted for classes and technical words remain everywhere else, so GracePeriod has a field called flag2 and the reader must switch languages mid-file.
  • A word is taken from the business that the business itself uses loosely, and the code inherits the ambiguity with the appearance of precision.
  • The glossary is written once and never maintained, so it becomes an authoritative-looking document that is wrong (Documentation Decay).
  • Every context is forced to share one vocabulary "for consistency", so the word "order" is stretched to cover three concepts and the model that results fits none of them (When Domain-Driven Design Does Not Pay).
Dependencies, and their direction
  • The code depends on the business vocabulary, deliberately and in that direction. That is the coupling being bought: it is the reason a bug report leads to code.
  • Each bounded context depends on its own words only, and on an adapter for anyone else's. Contexts that share vocabulary directly become coupled through it in a way nobody declared (Kinds of Coupling).
  • External APIs and stored rows depend on the vocabulary at the time of writing, which makes them a brake on renaming (Backward Compatibility as a Constraint).
Misreads
  • "So we have to do DDD." Ubiquitous language is nearly free and is independent of aggregates, repositories, domain events and every other tactical pattern. Take the vocabulary and leave the rest if the rest does not pay (When Domain-Driven Design Does Not Pay).
  • "One vocabulary for the whole company." That is the mistake bounded contexts exist to prevent. Two teams meaning different things by "order" is normal, and forcing one meaning produces a model that serves neither (Aggregates).
  • "Domain names mean no technical names." Infrastructure code should be named after infrastructure. ConnectionPool is not a domain concept and should not pretend to be one.
  • "The database column names are the domain language." They are the vocabulary of whoever wrote the first migration, frozen. Treat them as an external contract to be mapped, not as a source of truth (Schema Leakage).
Smells this explains
  • primitive-obsession
  • duplicate-knowledge

Testing it, and how it ages

What to test, and at which boundary
  • Test names in domain language double as executable specification, and a domain expert can read them and say "no, that is not what happens after a failed retry" — which is a review nobody else can perform.
  • Assert the state names the business uses, not the integers underneath, so a change to the storage representation does not rewrite every test (What a Unit Is).
  • Test the translation at each context boundary explicitly, because that is the one place where two vocabularies meet and a mapping can silently be wrong (Contract Tests).
How this design ages
  • Vocabulary sharpens as the business matures: one word splits into two, and the split usually arrives as a feature request rather than as a naming discussion. Recognising it as a naming event is the skill.
  • Terms that came from a product decision die with it. When "Pro plan" is renamed "Team plan", the internal name can lag briefly but should not lag permanently, or you accumulate a second vocabulary of historical names.
  • A codebase that renames with the business stays searchable for a decade. One that does not accumulates strata — you can date each module by which product generation's words it uses.

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.

  • DOMAIN-SPECIFICThe payoff scales with how much irreducible business rule there is. In insurance, payroll, logistics or trading the domain has decades of precise vocabulary and using it is transformative; in a CRUD admin tool over five tables there is barely a domain to speak, and "user", "project" and "file" are already the domain language.
  • GENERALThe mechanism — a reader who knows the domain can predict behaviour instead of deducing it — is a property of how people read unfamiliar code, so it holds regardless of language, framework or architecture style.
  • CONTESTEDThe serious counter-argument is that business vocabulary is unstable and often incoherent, so binding the code to it means a rename every reorg and importing genuine ambiguity into the type system. Practitioners who have lived through a company rebranding every noun twice argue that a stable technical vocabulary plus a maintained glossary is cheaper, and that translation at the support boundary is one lookup by one team rather than churn across the whole codebase. That is right for volatile marketing terms; it is wrong for the durable concepts — invoice, shipment, policy, settlement — that outlive every reorg.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — a vocabulary boundary is usually also a service boundary candidate, because the place two groups mean different things by a word is the place their models can diverge independently.