Good Naming Is the Cheapest Form of Documentation
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Cost Nobody Measures
When a developer opens an unfamiliar class and spends two minutes figuring out what mgr, proc, and tmp refer to before they can begin understanding the logic, that cost is invisible. It doesn't appear in sprint velocity. It doesn't trigger a Jira ticket. It just accumulates — across dozens of developers, hundreds of file opens, thousands of working hours — until the codebase has a reputation for being "hard to work with" and nobody can articulate exactly why.
Bad naming is a slow tax on your entire team's productivity. Good naming is the rebate.
The return on investment is asymmetric in the best possible way: the cost of choosing a clear name is about fifteen seconds of thought at write time. The benefit is paid forward every time anyone reads that code for the lifetime of the codebase.
What Good Names Actually Do
A good name does one thing: it tells the reader what to expect without requiring them to read the implementation. When a function is named getUserById, you know it takes an ID and returns a user. When it's named fetchUser, you know less — fetch from where? With what key? When it's named processData, you know almost nothing.
Good naming removes the need to read the body to understand the contract. This compounds at scale: a developer navigating a 50,000-line codebase reads mostly names — class names, method names, variable names. The implementation is read only when the name isn't sufficient. If the names are good, most implementation reads are unnecessary.
The Specific Habits
Name variables for what they contain, not how they're used
// What is this?
boolean flag = checkUserStatus(userId);
// Now you know without reading checkUserStatus
boolean isAccountSuspended = checkUserStatus(userId);
flag, result, temp, data — these are placeholders. They communicate that a value exists, not what the value is.
Name functions for what they do, including side effects
// Sounds like a query, actually sends an email
boolean checkAndNotifyUser(User user);
// Honest name
boolean notifyUserIfEligibleForUpgrade(User user);
If a function sends an email, writes to a database, or calls an external API, its name should reflect that. Functions named as pure queries that have side effects are a category of bug waiting to happen.
Name booleans as predicates
// Reads naturally in an if statement
if (user.isEligibleForDiscount()) { ... }
if (order.hasExpiredItems()) { ... }
if (transaction.wasProcessedSuccessfully()) { ... }
// Doesn't read naturally
if (user.eligibleDiscount()) { ... }
if (order.expiredItems()) { ... }
A boolean variable or function should read as a yes/no question. is, has, was, can, should — these prefixes communicate both the type and the intent.
Name constants to replace magic values
// What is 86400? Why this number?
if (sessionAge > 86400) expire();
// Immediately understood
static final int SESSION_MAX_DURATION_SECONDS = 86400;
if (sessionAge > SESSION_MAX_DURATION_SECONDS) expire();
Magic numbers and magic strings are unnamed assumptions. Constants are named constraints. When a constant needs to change, you change one thing in one place, and every reader knows what changed and why.
The Renaming Refactor
One of the most underrated refactors is a rename with no other changes. Take a function named handle() and rename it to validateAndPersistIncomingPaymentEvent(). No logic changes. The codebase is meaningfully better — every call site is now self-documenting, and the implementation is less likely to be modified incorrectly because the name communicates what it should and shouldn't do.
This kind of refactor is nearly zero-risk (with IDE rename support), takes under a minute, and pays dividends indefinitely.
The Naming Anti-Patterns Worth Calling Out
Encoding types in names: userList, orderMap, configString — when your type system already expresses the type, the name is redundant noise. The type annotation or generic parameter already says List<User>; userList adds nothing.
Abbreviation that saves four characters and costs comprehension: usrMgr, cfg, txn. The keystrokes you save in writing are not worth the cognitive overhead on every read. IDEs autocomplete. Abbreviations don't.
Names that reflect implementation rather than intent: processByForLoop, sortWithQuicksort — these expose how, not what. When the implementation changes, the name becomes misleading.
Vague domain terms: Manager, Handler, Processor, Service, Helper — these are not names, they are job descriptions. An OrderProcessor processes orders, which describes roughly half the codebase. OrderTaxCalculationService, OrderFulfillmentCoordinator — these communicate actual roles.
The Practical Takeaway
In your next PR, spend five minutes specifically reviewing names — not logic, not structure, just names. Find every instance of data, result, flag, temp, mgr, or similarly vague identifiers. Rename each one to describe what it actually contains. Submit the rename as a separate commit with no logic changes. Note in code review whether the diff is easier to read after than before.
It will be.