Skip to content

Lesson 2 of 9 · 15m

Naming and Scope: The Vocabulary of a System

Names carry your mental model to the next reader — usually future you, at 2 a.m., during an incident.

During an outage you grep for data and get 400 hits. You grep for temp and get 200 more. The code ran fine; it was unreadable under pressure, which is the only time readability matters. Names are the compression format your mental model survives in.

Names encode contracts

records says nothing; pendingInvoices says what, and hints at what it is not. Conventions that hold up:

  • Functions are verbs: fetchUser, parseHeader, revokeSession.
  • Data is a noun: order, allowedCidrs.
  • Predicates read as questions: isExpired, hasPermission, canRetry.

Match precision to scope. i in a three-line loop is fine; i surviving across forty lines is a bug farm — it will collide with something eventually, and no one will know which i it was.

One word per concept

If the domain says "quota," don't write limit, cap, and max interchangeably — three synonyms become three imagined concepts, and someday someone writes if limit > cap. Security vocabulary especially: sanitized, validated, trusted, and raw are claims. cleanData that was never cleaned is how injection slips through review — the name asserted a property nobody enforced.

Scope is a security control

Smallest possible scope, always: local over module, module over global, private over public. Every exported symbol is an API you must maintain and a surface an attacker or a careless caller can reach.

// before: a module-level cache any import can poison
export const userCache: Record<string, User> = {};

// after: state has an owner and an interface
export function makeUserCache() {
  const store = new Map<string, User>();
  return {
    get: (id: string) => store.get(id),
    put: (id: string, u: User) => store.set(id, u),
  };
}

Globals and ambient singletons hide data flow. In security review, taint flows through shared mutable state invisibly — the session map "everyone writes to" is where privilege bugs breed.

Naming also ages. A function called legacySync that became the primary path two years ago now lies to every new reader. When a name stops being true, renaming is not cosmetic — it is correcting the documentation the compiler can't check.

Try this: take a 50-line function you wrote recently and rename every variable as if handing it to a new hire on their first day. If a name needs a comment to be correct, the name is wrong — rename until the comment is redundant.

Pitfalls

  • Longer isn't better: theTemporaryUserAccountObjectInstance obscures as much as o.
  • Abbreviations rot: mgr, usr, tmp mean different things to every reader.
  • Consistency beats brilliance: a mediocre name used everywhere beats three clever names used once each.
  • Rename with your IDE's refactor-rename, not find-and-replace — that is how userId becomes userIde in a string literal somewhere.