skip to content

In what way is a hard-to-choose name design feedback, and what makes renaming expensive once an identifier has crossed a module or published-API boundary?

level: principalimportance: should knowfreq 30%

answer

  1. "And" in a function name = SRP violation
  2. no noun fits → missing abstraction (Manager, Helper)
  3. ubiquitous language; bounded context per meaning
  4. cost tiers: local → module → published API → wire/DB/metric
  5. expand → migrate → contract; instrument the old name

basics

~20 s

If you cannot name something without "And" or a vague word like "Manager", it probably does more than one job — the naming problem is really a design problem. Renaming is free inside a function, but a name in a public API, database column, event field, or metric is a contract others depend on.

solid answer

~50 s

Two points. **Naming as feedback:** difficulty naming a unit usually means the unit lacks a single responsibility (`processOrderAndNotify`), or the concept is missing from your model, or the code has drifted from the language the business uses. Aligning names with the domain experts' vocabulary — domain-driven design's *ubiquitous language*, scoped per bounded context so the same word may legitimately mean different things in different contexts — turns naming from decoration into modelling. **Renaming cost tiers:** local variable (free, compiler-checked) → internal module symbol (cheap, mechanical) → cross-team source dependency (coordinated, still compiler-checked) → published API, wire/event field, DB column, URL path, config key, metric, or log field (a breaking change with no compiler to catch it). At the last tier use expand–migrate–contract: emit or accept both names, migrate consumers, then remove the old one. The lesson: names are cheapest to get right at design review, before publication.

code

pseudocode · 12 lines
pseudocode
// expand-migrate-contract for an event field rename custId -> customerId

// 1. EXPAND: emit both; readers accept either
event = { custId: id, customerId: id }   // dual-write, backwards compatible

// 2. MIGRATE: consumers switch to customerId; backfill stored data;
//    update dashboards, alerts and saved queries;
//    instrument reads of custId so remaining usage is observed, not guessed

// 3. CONTRACT: once custId reads are provably zero for longer than the
//    slowest consumer's release cycle, drop it
event = { customerId: id }

go deeper

for a junior

Say that a name containing "and" usually means the function does two things, and that renaming something other code depends on can break that code.

for a middle

Distinguish internal renames (safe, compiler-checked) from public-API renames (breaking), and mention deprecation before removal.

for a senior

Lay out the full cost tiers including wire/DB/metric/config names where nothing is compiler-checked, describe expand–migrate–contract concretely, and connect naming difficulty to single responsibility and missing abstractions.

for a principal

Add governance and modelling: ubiquitous language with bounded contexts and translation at the seams, names reviewed at API-design time, a glossary plus lint/architecture enforcement, and the explicit trade-off of freezing a legacy wire name while renaming the internal model behind a mapping layer.

## Part 1 — naming difficulty is design feedback When a name is hard to choose, the usual reflex is to reach for a filler word. That reflex hides useful information. Three distinct diagnoses: 1. **The unit does more than one thing.** The tell is a conjunction: `processOrderAndSendEmail`, `validateAndSave`, `parseAndExecute`. The honest name has an "and" in it, and "and" in a function name is a direct statement that the single-responsibility principle is violated. The fix is to split until each piece has a short, honest name. 2. **A concept is missing from the model.** The tell is a vague suffix — `Manager`, `Helper`, `Util`, `Handler`, `Processor`, `Data`, `Context` — used because no noun fits. Very often the reason no noun fits is that the abstraction has not been discovered yet. `OrderManager` with 40 methods usually decomposes into `OrderPricing`, `OrderValidation`, `OrderRepository`, `OrderStateMachine` — names that only become available once you look for them. 3. **The code and the business speak different languages.** The tell is that engineers say "the flag on the record" while the business says "the policy is in grace period". Translation between the two vocabularies happens in every conversation and every handoff, and each translation is a chance to lose a requirement. ### Ubiquitous language and bounded contexts Domain-driven design's answer to (3) is the **ubiquitous language**: one shared vocabulary used identically by domain experts, in conversation, in the specs, and in the code's identifiers. If the business says "settlement", the class is `Settlement`, not `PaymentFinalizer`. The payoff is that requirement discussions and code reviews use the same words, so mismatches surface as arguments about the *domain* rather than as silent mistranslations. The necessary companion idea is the **bounded context**: a boundary within which one meaning of a term holds. "Customer" in billing (a party with a payment method and tax status) is genuinely not "Customer" in support (a person with a contact history). Forcing one shared `Customer` class across both contexts produces a bloated model that serves neither. Different contexts may legitimately use the same word for different things — as long as the boundary is explicit and translation happens at the seam (an anti-corruption layer). This is the mature nuance: *global* consistency of vocabulary is not the goal; consistency *within a context*, with explicit translation between contexts, is. ## Part 2 — the cost curve of renaming Renaming is not one operation with one price. It has tiers, and the tier is set by who can see the name and whether a compiler checks it. | Tier | Example | Who breaks | Safety net | |---|---|---|---| | Local | a variable inside a function | nobody | compiler + IDE, atomic | | Module-internal | a private class or method | your module | compiler, mechanical | | Cross-module, same repo | a public class in a shared library | other teams' code | compiler, needs coordination | | Published source API | a library released to consumers | downstream builds | compiler *at their build time*; needs a deprecation cycle | | **Wire / persisted / operational** | JSON event field, DB column, URL path, config key, metric name, log field, feature-flag key | consumers, dashboards, alerts, saved queries, stored data, runbooks | **none — silent failure at runtime** | The last row is where the real cost lives, and it is the row candidates most often forget. A JSON field rename does not fail to compile; it produces a null, a default, or a dropped record in a downstream consumer, possibly weeks later. A metric rename silently empties a dashboard and disables an alert — the alert does not fire *because it no longer matches anything*, which is the worst failure mode in operations. A database column rename must contend with data already written under the old name, with in-flight deploys where old and new code run simultaneously, and with rollback. ### Expand–migrate–contract (a.k.a. parallel change) The standard safe rollout for a boundary rename: 1. **Expand** — introduce the new name alongside the old. Writers emit both fields / dual-write both columns; readers accept either. Nothing breaks; the change is backwards compatible and rollback-safe. 2. **Migrate** — move consumers to the new name; backfill persisted data; update dashboards, alerts, and saved queries. Instrument the old name so you can *observe* remaining usage rather than guess. 3. **Contract** — once usage of the old name is provably zero for longer than your longest consumer's release cycle, remove it. Supporting tools: deprecation annotations with a stated removal version, schema-registry compatibility checks for event schemas, API versioning when the change is too large for aliasing, and consumer-driven contract tests that fail *your* build when you break *their* expectation. ### The governance conclusion Because the cost curve is so steep, the leverage is entirely at the front: **review names at API-design time**, before the first consumer exists. Concretely, that means a design review step that scrutinises public identifiers, event field names, and URL shapes; a written glossary tied to the ubiquitous language; linters and architecture tests that enforce layer suffixes and ban noise words; and a norm that internal names are cheap to fix continuously (so nobody hoards renames) while boundary names are treated as versioned contracts. A final trade-off to name explicitly: **do not let a bad boundary name become permanent just because renaming is expensive.** Weigh the ongoing comprehension and mis-integration cost of a misleading public name against a one-off expand–migrate–contract. Sometimes the right call is to keep the wire name stable and rename only the internal model, translating at the boundary — the mapping layer absorbs the legacy vocabulary so it stops infecting the domain code.

  • Why is renaming a metric or log field arguably riskier than renaming a public method?
    Because there is no compiler and the failure is silent and inverted: a renamed metric leaves dashboards empty and, critically, stops alerts from matching — so the alert simply never fires. A renamed public method at least breaks a downstream build loudly at a known time.
  • Is global naming consistency across a large system always the goal?
    No. Within a bounded context, consistency is essential. Across contexts, the same word can legitimately mean different things — "Customer" in billing versus in support — and forcing one shared model produces something that serves neither. The discipline is to make the boundary explicit and translate at the seam rather than to unify the vocabulary.
  • A public wire field has a bad name but renaming it would disrupt three consumer teams. What do you do?
    Weigh the ongoing mis-integration and comprehension cost against a one-off expand–migrate–contract. A common middle path is to keep the wire name frozen and rename only the internal domain model, mapping at the boundary — the translation layer absorbs the legacy vocabulary so it stops spreading into the domain code.

Renaming a local variable is retitling a note on your own desk. Renaming a public event field is changing a train station's name: the sign is easy, but every timetable, ticket, map and alarm clock in the country references the old one — and they fail silently, by taking people to the wrong place.

saying these in an interview costs you the question

  • Treating all renames as equally cheap because "the IDE does it".
  • Renaming a JSON field, DB column, metric, or config key in place and announcing it in chat.
  • Insisting on one global vocabulary across all bounded contexts, producing a bloated shared model.
  • Deciding a bad public name is permanent purely because renaming is hard, with no cost comparison.
  • Removing the old name on a fixed date without instrumenting whether anyone still reads it.
  • Treating a hard-to-name class as a naming problem to brainstorm rather than as evidence of a responsibility problem.

context