open atlas
↑ Back to track
Code patterns & craft CP · 01 · 03

Consistency and scope

Use one word per concept across the codebase — mixed synonyms are hidden coupling readers must decode — and let name length scale with scope: terse in tiny locals, descriptive at module boundaries.

CP Middle ◷ 18 min
Level
FoundationsJuniorMiddleSenior

You open a service and read getUser, then three files later fetchUser, then retrieveUserById, then loadCustomer. Are these four operations on four concepts, or one operation on one concept spelled four ways? You can’t tell from the names — so you go read all four implementations to find out they’re the same thing. That reading tax is paid by every person who ever touches this code, forever.

A name is not just a label on one symbol. It is a promise about how that symbol relates to every other name in the system. When the vocabulary is inconsistent, the reader has to learn that fetchX and getX mean the same thing — and that learned mapping is coupling that lives in their head instead of in the code.

Goal

After this lesson you can apply two naming forces a senior engineer balances constantly: keeping one word per concept across a codebase (a shared, ubiquitous vocabulary, which is a form of low coupling) and scaling name length to scope (terse where the reader can see everything, descriptive where they can’t). You’ll also be able to name the failure mode of each rule — when forcing consistency or forcing descriptiveness actively makes code worse.

1

One word per concept — synonyms are hidden coupling. Pick exactly one verb for an operation and one noun for a domain entity, then use them everywhere. If “get the user” is getUser, then it is getUser in the repository, the service, the controller, and the test — never fetchUser here and retrieveUser there. The same goes for nouns: if the domain calls the entity a Customer, it is Customer end to end, not User in auth, Customer in billing, and Account in the dashboard.

// Inconsistent vocabulary — three words, one concept
const a = await userRepo.fetchUser(id);
const b = await userService.getUser(id);
const c = await cache.retrieveUserById(id);

A reader scanning this cannot assume fetch, get, and retrieve are interchangeable — distinct words signal distinct behaviour. So they must open each one to confirm “these are all just a keyed read.” That confirmation step is pure waste, and it scales with every synonym you introduce.

2

A shared vocabulary is low coupling; a split vocabulary is connascence of meaning. When User, Customer, and Account all denote the same thing, every reader and every refactoring tool must carry the mapping User ≡ Customer ≡ Account. That shared understanding is an invisible dependency between modules: change what Customer means in billing and you’ve silently changed an assumption the auth module relies on, because they were “the same” only by convention, not by type.

// Three names, one entity → an unwritten contract readers must memorise
function authenticate(user: User): Session { /* ... */ }
function chargeCard(customer: Customer): Receipt { /* ... */ }
function renderHeader(account: Account): Html { /* ... */ }

One ubiquitous term (Customer everywhere, say) collapses three mental models into one and lets the type system enforce the relationship instead of the reader’s memory. This is why domain-driven design pushes a ubiquitous language: shared words are the cheapest decoupling you can buy.

3

Name length should scale with scope. The right length for a name is a function of how far it travels and how much context the reader already has. In a three-line loop, the reader can see the entire lifetime of i at a glance, so i is not just acceptable — a longer name there is noise that hides the loop’s shape. At the other extreme, an exported function travels to call sites that have none of this file’s context, so its name must carry the meaning on its own.

// Tiny scope: i and x are perfect — the whole story fits in the eye
for (let i = 0; i < points.length; i++) {
  const x = points[i].x;
  sum += x;
}

// Wide scope: the export must explain itself with zero local context
export function totalRevenueForFiscalQuarter(orders: Order[]): Money { /* ... */ }

The rule of thumb: short names for short-lived, narrow-scope locals; long, intention-revealing names for anything that crosses a function, module, or package boundary. Scope is the dial; length follows it.

4

Each rule has a failure mode — applying it blindly makes code worse. Consistency and descriptiveness are forces, not absolutes, and over-applying either is a real smell.

  • Forcing long names into tiny scopes adds noise. for (let currentIterationIndex = 0; ...) or const userObjectFromArray = points[currentIterationIndex] buries a trivial loop under ceremony. The reader now parses long identifiers to recover a structure that i and x showed instantly.
  • Forcing terse names into wide scopes hides intent. An exported function calc(d: Data): R tells a distant caller nothing — they must open it to learn what it computes, which is exactly the cost descriptive names exist to remove.
  • Forcing false consistency couples unrelated things. If two operations genuinely differ — fetchUser does a network call with retries, getUser is a pure cache lookup — collapsing them to one word erases a distinction the reader needs. Consistency means one word per concept, not one word regardless of concept.
// Over-descriptive local: ceremony obscures a 3-line loop
for (let currentIndexInLoop = 0; currentIndexInLoop < items.length; currentIndexInLoop++) { /* ... */ }

// Under-descriptive export: terse where the reader has no context
export function proc(o: Order[]): number { /* ... */ }

The senior move is to read the scope first, then choose the length and the word — never to apply “always be descriptive” or “always reuse the same verb” mechanically.

Worked example

Both forces in one module. Before — inconsistent vocabulary at the boundary, ceremonial names in the locals:

// orders.ts
export function calc(ordersArrayParameter: Order[]): number {
  let runningTotalAccumulatorVariable = 0;
  for (let loopIndexCounter = 0; loopIndexCounter < ordersArrayParameter.length; loopIndexCounter++) {
    const currentOrderBeingProcessed = ordersArrayParameter[loopIndexCounter];
    runningTotalAccumulatorVariable += currentOrderBeingProcessed.amount;
  }
  return runningTotalAccumulatorVariable;
}

// elsewhere — same concept, three different verbs
const totalA = calc(await repo.fetchOrders(uid));
const totalB = calc(await svc.getOrders(uid));
const totalC = calc(await cache.loadOrders(uid));

The export calc is too terse for its wide scope — a caller can’t tell what it sums. The locals (loopIndexCounter, currentOrderBeingProcessed) are too verbose for a three-line body. And the read operation has three names (fetch/get/load), so the reader must verify they’re the same.

After — descriptive at the boundary, terse in the locals, one verb for one concept:

// orders.ts
export function totalOrderAmount(orders: Order[]): number {
  let total = 0;
  for (const order of orders) {
    total += order.amount;
  }
  return total;
}

// one verb for "read orders" everywhere
const totalA = totalOrderAmount(await repo.getOrders(uid));
const totalB = totalOrderAmount(await svc.getOrders(uid));
const totalC = totalOrderAmount(await cache.getOrders(uid));

The export now states its meaning to a context-free caller; the loop locals (total, order) are short because the whole loop fits in view; and getOrders is the single word for one concept, so the reader never decodes synonyms. Nothing here is cleverer — it just matches each name’s length to its scope and uses one word per concept.

Why this works

Why treat inconsistent vocabulary as coupling rather than mere style? Because coupling is “a change here forces a change in your understanding there.” When User and Customer are the same thing under two names, every reader maintains the equivalence in their head, and every rename or behavioural change has to be reconciled against an unwritten contract. That is connascence of meaning: two parts of the system agree on a convention that the compiler can’t see. Folding synonyms into one ubiquitous term turns that head-coupling into a single typed symbol the tools can track and refactor — which is the whole point of low coupling.

Common mistake

The most common over-correction is “names should always be as descriptive as possible,” which produces currentIterationIndexValue in a two-line loop and theUserThatWeAreCurrentlyProcessing for a one-statement lifetime. Descriptiveness is a scope-relative virtue, not an absolute one: it pays off precisely where the reader lacks local context (exports, module APIs, long-lived fields) and becomes pure noise where they have all the context they need (tight loops, short lambdas, throwaway temporaries). Don’t measure a name by how much it says — measure it by whether it says the right amount for how far it travels.

Check yourself
Quiz

A codebase reads a user via getUser in the service, fetchUser in the repo, and retrieveUser in the cache — all three are the same keyed read. Why does a senior engineer call this a coupling problem, not just a style nit?

Recap

Two naming forces work at different scales. Across the codebase, use one word per concept: synonyms like get/fetch/retrieve for one operation, or User/Customer/Account for one entity, force every reader to carry an unwritten “these are the same” mapping — that is hidden coupling (connascence of meaning), and a shared ubiquitous vocabulary is the cheapest way to remove it. Within a span, scale name length to scope: terse names (i, x, total) where the reader sees the whole lifetime, descriptive names where the symbol crosses a function, module, or package boundary with no local context. Each rule has a failure mode — long names crammed into tiny scopes add noise, terse names exposed at wide scopes hide intent, and false consistency erases real distinctions. The skill is to read the scope first, then choose the word and its length to match.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.