DomainGENERALSCALE-SPECIFICCONTESTED

Ubiquitous Language

One word for one concept, in conversation and in code — where the concept is real. The failure is three names for one thing across three modules, all of them defensible.

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

When is it worth forcing the code to use the business's word, and when is a separate technical name the honest one?

The requirement

A support engineer asks why a "hold" was not released. Billing calls it a PaymentAuthorization, the warehouse service calls it a StockHold, the admin UI calls it a "reservation", and all three are the same row.

The obvious build

Each module names things in whatever way is clearest to the people who work on it. Local clarity is what matters, and translating at the edges is a small cost.

Why it breaks

The translation is not written down anywhere, so it lives in the two engineers who have worked on both modules — and becomes a bus-factor problem the moment either leaves (Bus Factor).

How it breaks as requirements change
  • The translation is not written down anywhere, so it lives in the two engineers who have worked on both modules — and becomes a bus-factor problem the moment either leaves (Bus Factor).
  • The three names diverge in meaning, slowly and invisibly. StockHold starts including manual warehouse holds that have no payment; PaymentAuthorization starts being created for zero-value orders. Now they are three concepts wearing one row, and nobody decided that.
  • A requirement phrased as "holds should expire after an hour" is ambiguous across all three, so it gets implemented in one and not the others, and the bug report six weeks later is about the other two.
  • Onboarding cost grows superlinearly: a new engineer must learn three vocabularies and the undocumented mapping, and cannot tell which differences are meaningful.
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
  • Two of the three names are in public API responses and cannot be changed without a version.
  • The warehouse vocabulary comes from a vendor WMS whose documentation the team must keep reading.
  • Renames touch every file that mentions the word, so each one is a large diff during a period of heavy feature work.
Invariants
  • A word used in a design conversation must resolve to exactly one thing in the code, or the conversation is not about the code.
  • When the business changes the meaning of a word, exactly one place in the code has to be re-read to find out what breaks.

Who owns what, and where the seams fall

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

Responsibilities
  • Whoever owns a concept owns its name, and owns saying so when the business word changes.
  • The edge adapters own translation to and from vendor and wire vocabulary — deliberately, in one place, rather than by everyone informally (Boundary Adapters).
  • The published API owns its own names permanently, because they are a promise; the internal model is free to be renamed (API Stability).
Boundaries
  • Inside a module, one word per concept. Across a module boundary, translation is allowed and often correct — this is exactly what an anti-corruption layer is for (Anti-Corruption Layer).
  • The vendor WMS is outside the boundary. Its vocabulary should stop at the adapter, not leak into the model, because it will change when the vendor does.
  • The line to hold is between concepts that are genuinely the same and concepts that merely correlate. Forcing one name onto two concepts is worse than three names on one.

Three names, one row, and where they quietly disagree

The exercise that resolves this is not a naming debate. It is a table: every name, what it currently includes, and the case where they differ. Almost always, two names turn out to be the same thing and the third turns out to be a genuinely different concept that had been hiding.

The last column is the one that decides. If two names never disagree, unify them. If they disagree on a real case, you have found two concepts and unifying them would be the more expensive mistake.

  • Two of the three are one concept: the admin label is a synonym and should go.
  • The third is not. A payment authorization exists without stock, and stock is held without payment, so merging them under one word would create a type with two lifecycles.
  • The finding is worth more than the rename: the team believed there was one concept and there were two, which explains the "hold was not released" bug directly.
NameModuleWhat it includes todayWhere it disagrees with the others
PaymentAuthorizationbillingA card authorization with an amount and an expiry from the PSP.Exists for zero-value orders where no stock is held at all. Genuinely a different concept.
StockHoldwarehouseA quantity of a SKU held at a location, with an expiry.Also created by warehouse staff manually, with no order and no payment.
"reservation"admin UIWhatever the warehouse row says, relabelled for support staff.Never disagrees — it is a display name for StockHold, so it is one concept with two names.

The smell: a synonym nobody declared

The recognisable symptom is a conversation where two people agree and are talking about different things. In code it shows up as a mapping function with no logic in it — a translation from one vocabulary to another that adds nothing but a name.

It is a smell rather than a defect because there is an entirely legitimate version of the same code, and telling them apart is the actual skill.

smellUndeclared synonym

looks like A function whose whole body is a field-for-field copy into a differently-named type — toStockHold(auth) mapping five fields onto five fields with no transformation — plus a display layer that renames it again.

suggests One concept has picked up a second name because two modules were written by different people at different times. Every requirement about the concept now has to be applied twice, and the two copies drift.

fix Delete the synonym inside your own boundary and keep one name. At an external boundary, keep the mapping but make it obviously an adapter — put it in the adapter module, not in the model — so the next reader knows the duplication is deliberate.

when this is fine It is genuinely correct at a boundary you do not control. Mapping the vendor WMS's HoldRecord onto your InventoryReservation is a field-for-field copy with no logic, and it is exactly right: it is the wall that stops the vendor's vocabulary and their next breaking change from reaching your model (Anti-Corruption Layer). The test is whether the two sides can be renamed independently. If they can and must, keep the mapping.

Converging a name without a three-week rename

The rename fails when it is attempted as one heroic pull request across a codebase under active development. Done as an ordered sequence with a behaviour-preserving step at each stage, it is boring, which is the goal.

The step people skip is the first one, and it is the only one that produces information. Renaming before establishing whether the names describe one concept is how two concepts get welded together.

From three names to one, safely
  1. 1
    Enumerate and diff

    List every name for the suspected concept and the cases where they include different things.

    fails by Skipping straight to a name vote, and merging two concepts that disagree on a real case.

  2. 2
    Decide with the business

    Ask the people who say the word which one they mean, and take their word literally.

    fails by Engineers choosing the name they find most precise, which is then never used in a meeting.

  3. 3
    Rename mechanically

    One commit, tool-driven, no behaviour change, tests untouched.

    fails by Slipping a behaviour fix into the rename, making the diff unreviewable (Review Size).

  4. 4
    Pin the boundary

    Add an explicit mapping and a test at every published surface so external names do not move.

    fails by Forgetting a queue message or an analytics event, which are surfaces nobody thinks of as an API.

  5. 5
    Update the human artefacts

    Dashboards, runbooks, alert names, the on-call doc.

    fails by Leaving an alert firing under the old name so on-call cannot find the code it refers to.

The fourth and fifth steps are where renames actually hurt teams, and they are the two that are invisible from inside the repository.

How to build it

Most important first.

  • Find the divergence before renaming anything: list every name, what it currently includes, and one case each where they disagree. If they disagree nowhere, it is one concept and needs one name.
  • Pick the word the business says out loud, not the one an engineer preferred, and not the vendor's. The point is that a requirement in their sentence is greppable (Naming and Domain Language).
  • Rename in one mechanical commit with no behaviour change, so review is a diff of identifiers rather than a design argument (Rename).
  • Keep the old name at the published boundary with an explicit mapping, so external callers are unaffected (Versioned Interfaces).
  • Do this where the word is contested and load-bearing. Renaming utils to helpers is not this exercise, and neither is imposing domain vocabulary on code that has no domain in it (When Domain-Driven Design Does Not Pay).

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
  • Before: "holds expire after an hour" costs a discovery phase across three modules, three implementations that may disagree, and a support burden when they do.
  • After: it is one grep for one word, and every hit is the same concept. The change is bounded by construction rather than by memory.
  • The cost that remains: every change at the published boundary now also touches the mapping, and the mapping is a place a rename can be forgotten. That is a permanent, small tax paid in exchange for removing an unbounded one.
What the recommended approach costs
  • Renaming is a large, risky-looking diff with no user-visible benefit, and it competes with feature work for review attention.
  • Insisting on business words makes some code clumsier: technical concepts genuinely have no business name, and forcing one produces euphemism.
  • A shared vocabulary across many teams needs maintenance — meetings, a glossary, someone who cares — and at some size that cost exceeds the confusion it prevents.

What can go wrong

Failure modes
  • Two genuinely different concepts get merged under one name because the vocabulary exercise treated naming as the goal, and the resulting type has two reasons to change (Divergent Change).
  • The rename is done in the code and not in the conversation, so engineers say "reservation" and ops still says "hold", which is the same problem with the participants swapped.
  • A glossary document is written, becomes stale in a quarter, and is then cited in arguments — a stale glossary is worse than none, because it has authority (Documentation Decay).
  • The team applies the vocabulary rule to technical code as well, so the HTTP client is renamed to something domain-flavoured and now nobody can find it.
Dependencies, and their direction
  • The internal model depends on the business's vocabulary; that dependency is intentional and is the whole idea.
  • The adapter depends on both vocabularies, which is why it is the only place allowed to know the vendor's.
  • Documentation, dashboards and runbooks all depend on the name, so a rename has a cost outside the codebase that engineers routinely forget (Documentation Decay).
Misreads
  • "Every name in the codebase must be a business term." No. The domain vocabulary applies to domain concepts. A retry policy, a connection pool and a serializer have no business names and should not be given fake ones.
  • "One name everywhere in the company." Different bounded parts of a business legitimately mean different things by "customer". The goal is one name per boundary with explicit translation, not global uniformity.
  • "The glossary is the artefact." The artefact is the code. A glossary is a snapshot that decays; the code is the thing that has to be right (Docs Close to Code).
Smells this explains
  • duplicate-knowledge
  • divergent-change

Testing it, and how it ages

What to test, and at which boundary
  • A test that the adapter maps every internal state to a published name and back, so the mapping cannot silently lose a case (Contract Tests).
  • Assert behaviour, not names — a rename must not change any test's expectations, and if it does, the rename was not mechanical (What Refactoring Actually Is).
  • Where the API name is frozen, one test that pins the wire vocabulary explicitly, so an internal rename cannot leak out.
How this design ages
  • Vocabulary drifts because businesses change. The healthy pattern is that a word changes in a meeting and in the code in the same week; the unhealthy one is a five-year-old class name nobody says any more.
  • When one word starts meaning two things in two parts of the business, that is the signal to split — the code should follow with two names, not compromise on one.
  • As the system grows past one team, forcing a single vocabulary system-wide becomes counterproductive; per-boundary vocabularies with explicit translation is the version that scales.

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 an ambiguous word makes a requirement ambiguous is independent of language and paradigm; only the cost of renaming differs, and modern tooling has made that cost much lower than the literature assumes.
  • SCALE-SPECIFICAt one team, a single vocabulary is achievable and clearly worth it. Past roughly four teams, enforcing one company-wide vocabulary costs more coordination than it saves and the correct unit becomes one vocabulary per boundary with translation between them.
  • CONTESTEDThe strongest opposing view is that chasing business vocabulary produces churn without behaviour change, and that names matter far less than tests and types: a well-tested StockHold is more useful than a poorly-tested InventoryReservation. Engineers making this argument are usually reacting to rename-only pull requests that broke dashboards, which is a real cost this lesson has to admit.

Where the depth lives

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

Architecturemodular-monolith
Domains that do not exist yet
  • Testing & Reliability Engineering — a rename is only safe if the suite would notice a behaviour change slipped in alongside it, which makes this one of the clearest cases where test coverage buys refactoring freedom.