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:
theTemporaryUserAccountObjectInstanceobscures as much aso. - Abbreviations rot:
mgr,usr,tmpmean 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
userIdbecomesuserIdein a string literal somewhere.