Skip to content

Naming Smells

A systematic guide for diagnosing and fixing naming smells. Names are the primary documentation readers encounter at every call site. A bad name makes every use of the code harder to understand; a good name makes the code self-documenting.

Source: Generalized from the PHP/JS-focused rc-fixing-naming-smells skill, supplemented with principles from Clean Code and community best practices.

Before diagnosing specific smells, internalize these principles. They apply to every identifier you write or review.

A reader who sees the name should be able to guess what it does. A reader who sees what it does should be able to guess the name. If either direction surprises, the name is wrong.

If the honest one-sentence description of what this thing does needs the word “and”, the thing is doing two things. The name can’t fix that — a split can.

EngineBinary (what it is) beats EngineProvider (what it plays). Role labels — *Service, *Manager, *Handler — are placeholders the author left behind when they didn’t know what to call the thing.

4. Verbs for Doers, Nouns for Things, Predicates for Booleans

Section titled “4. Verbs for Doers, Nouns for Things, Predicates for Booleans”
  • Functions/methods are usually verbs: validate(), format(), calculateTax().
  • Classes that hold state or represent entities want nouns: Binary, Artifact, Invoice.
  • Classes that are primarily actors on a domain object may wear a verb-er suffix under strict discipline (see Class Naming below).
  • Booleans want a predicate prefix: isValid, hasPermission, canRetry, shouldNotify.

Engine\Binary reads better than Support\EngineBinary. Don’t repeat in the name what the folder already says. Don’t put a thing in a folder that contradicts its name.

# Bad: namespace repetition
from engine.calc import EngineCalculator
# Good: namespace provides context
from engine.calc import Calculator

A name that looks elegant in the file header but reads awkwardly at every call site is failing its actual readers. Always test candidates at the call site.

Things in the same scope should be named at the same level of abstraction. Calculator, CalculatorInterface, HouseholdCalculation in one directory is asymmetric — three different framings of “the calc thing.”

A widely-used public class deserves a precise (sometimes longer) name. A private helper used once can be short. Don’t make a five-word name to compensate for a vague concept — find the noun instead.

BaseX, AbstractX, DefaultX, SimpleX only earn their prefix if there’s a real sibling that justifies the contrast. If there isn’t, drop the prefix.

10. Leave the Codebase One Workaround Lighter

Section titled “10. Leave the Codebase One Workaround Lighter”

A good rename lets at least one consumer drop an awkward variable name or a “what is this?” comment. If nothing downstream gets simpler, reconsider.


Part 1: Class / Type / Module / Interface Naming

Section titled “Part 1: Class / Type / Module / Interface Naming”

Classes, interfaces, and modules have the largest blast radius. A bad class name poisons every import, every variable declaration, every call site. The rigor here is highest.

Suffixes fall into two tiers.

Tier 1 — Placeholder Suffixes (always reject)

Section titled “Tier 1 — Placeholder Suffixes (always reject)”

These convey no specific responsibility. The author didn’t know what the class does.

Suffix Why it’s empty
*Service “Service” means everything and nothing
*Manager Manages what, how?
*Handler Handles what, how?
*Helper A drawer of unrelated things
*Util / *Utility Same — unrelated grab-bag
*Support Supports what?
*Processor Processes what, how?
*Wrapper Wraps what, why?
*Info / *Data “Info” is not a domain concept

Rule: Reject every candidate that ends in one of these. Find the noun that names what the class actually is or does.

Tier 2 — Verb-er Suffixes (conditionally OK)

Section titled “Tier 2 — Verb-er Suffixes (conditionally OK)”

These name a real, single-purpose verb. Accept only when all three hold:

  1. Single-purpose verb. The class does one verb on one kind of thing.
  2. Concrete domain prefix. EngineRunner ✓. DataRunner ✗ (“data” isn’t a thing).
  3. Can’t fold onto the domain object. If format() belongs on the thing being formatted, don’t make a separate Formatter.

Accepted verb-er suffixes (with justification): Runner, Builder, Validator, Resolver, Formatter, Locator, Seeder, Initializer, Reader, Writer, Loader, Parser.

// Tier 2 — justified: single verb, concrete prefix, can't fold onto EngineBinary
class EngineRunner { run(binary: EngineBinary): Result {} }
// Tier 2 — justified: single verb, concrete prefix
class CategoryLineFormatter { format(lines: string[]): string {} }
// Tier 1 — rejected: "Processor" says nothing
class DataProcessor { process(data: any): any {} }
// Fix: find the noun. What does it actually do?
class SubscriberFilter { filterActiveWithEmail(records: Subscriber[]): Subscriber[] {} }
Smell Signal Fix
Placeholder class Name contains Tier 1 suffix (Helper, Manager, Service) Read the public surface; name what it actually does
Multiple responsibilities Public methods fall into 2+ unrelated groups; honest summary needs “and” Don’t rename — split. Propose new classes with clear boundaries
Interface misnamed XInterface + class X — interface named after class instead of contract Name the contract (CategoryContract) or name the interface X and rename the class
Mechanism in name Name describes how it works, not what it is (HouseholdCalculation for a class that stores artifacts) Rename to the artifact it produces: CalculationStore, CalculationArtifact
Misplaced static utility Single-method static class (StringHelper::format()) Move the method onto a domain object, or rename to the verb (LineFormatter)
Wrong folder Well-named class in a folder that contradicts its purpose Keep the name, move the file. Group related concepts (Engine/{Binary, Runner})
Vocabulary collision Name reuses a term that already means something different in the codebase Pick a different noun, or rename the existing user first
Brand-new term Name introduces a term that appears nowhere else in the codebase Justify in one line why no existing term fits, or reach for an existing term
Abstract/Base qualifier AbstractX or BaseX with only one concrete implementation Drop the prefix or inline the abstract class
Namespace repetition Engine\EngineRunner, Calc\CalculatorService Drop redundant prefix: Engine\Runner
Misfit method One method doesn’t quite fit the proposed name The misfit is the signal it belongs elsewhere. Flag it, propose its real home — don’t widen the name to swallow it
  1. List the public surface. What methods/properties does this class expose?
  2. State what it does in one sentence. Use only verbs/nouns from the surface. If you need stuff, things, or helpers, the current name doesn’t match.
  3. Compare to the current name. Note the gap.
  4. Survey call sites. For each usage: what variable name does the caller pick? Any nearby comment explaining what it does? A variable name like $binaryResolver = new EngineHelper() is a compensating name — it proves the class name failed.
  5. Diagnose self-containment at each call site. The current name fails if any of:
    • Caller picks a compensating variable name ($binaryResolver = new EngineHelper())
    • A comment clarifies what the call actually does
    • A reader can’t predict the effect from $variable->method(...) alone
  6. Propose candidates. 1–3 names, each with one line of rationale tied to behavior.
  7. Vocabulary-check each candidate. Grep the codebase for the term. If it already means something different → reject. If it’s brand new → write one line justifying why no existing term fits.
  8. Replay at every call site. Mentally substitute: does every call site read naturally without compensating variable names or comments?
  9. If no candidate is self-contained — the class is doing too much. Propose a split, don’t pick a vaguer name.

Functions and methods wear the verb. A bad verb forces every caller to read the body to understand what happens.

Reject every candidate containing these verb or noun roots:

Type Placeholders
Verb roots process*, handle*, manage*, do*, perform*, execute*
Noun roots *Data, *Info, *Stuff (when used as the function’s noun)

processData() is process + Data — both placeholders. It says nothing.

The test: At the call site, does result = name(args) predict the body?

# Bad: placeholder verb + placeholder noun
targets = process_data(subscribers) # what happened to subscribers?
# Good: specific verb on specific noun
eligible = filter_active_with_email(subscribers)
Smell Signal Fix
Placeholder verb process(), handle(), manage(), do(), perform(), execute() Read the body; name what it actually does
Placeholder noun processData(), handleInfo() — generic verb + generic noun Name the specific verb on the specific noun
Repeats class noun User::getUserName(), Order::orderTotal() The class supplies the noun. Drop it: User::name(), Order::total()
Boolean without predicate valid(), done(), loaded() Reads as a command. Fix: isValid(), hasFinished(), isLoaded()
Does two things Honest summary needs “and”: “filters records and projects to log entries” Split into filter* + project*
Callers disagree on return targets = f(...), cleaned = f(...), entries = f(...) — different variable names Function name doesn’t tell callers what they got. Rename so they pick the same word
Misleading verb save() that actually upserts; get() that creates if missing Name the actual operation: upsert(), getOrCreate()
Vestigial get/set getName() on a data class with no side effects, in a language with property syntax Drop the prefix: user.name (property/getter)
  1. Read the body. State what it does in one sentence, using verbs/nouns from the body.
  2. Compare to the current name. Note the gap. Apply Suffix Discipline.
  3. Survey call sites. What variable name do callers pick for the return value? Do different callers pick different names?
  4. Propose candidates. 1–2 names. Replay: does result = candidate(args) predict the body?
  5. If no candidate fits — the function is doing too much. Propose a split. Common triggers:
    • Summary needs “and”
    • Two distinct passes (filter then project; fetch then transform)
    • Different callers want different slices

Variables live within one function — the smallest scope, but also the most frequent. A bad variable name makes every line that uses it ambiguous.

Reject these names (outside one-line scopes): data, info, temp, result, obj, item, arr, val, stuff.

The test: At the use site, can a reader predict what’s stored from the name alone?

# Bad: placeholder
data = filter(...)
await send_newsletter(data) # what is data?
# Good: role-named
recipients = filter(...)
await send_newsletter(recipients)
Smell Signal Fix
Placeholder data, info, temp, result, obj, item, arr, val, stuff Name the role: activeUsers, dispatchLogEntries
Type-tagged userObj, userArr, userMap, usersList The type system already tells you the type. Drop the tag: user, users, usersByEmail
Boolean without predicate valid, done, loaded isValid, hasFinished, isLoaded
Hungarian notation strName, iCount, bActive in a typed language Drop the prefix: name, count, isActive
Single-letter r["email"], r["status"] — throwaway name for a non-throwaway value Use the domain noun: subscriber
Cardinality mismatch user = users.filter(...) returning a list Match the type: activeUsers for array, firstActiveUser for single
Vestigial qualifier allUsers when there’s no someUsers; rawInput when there’s no cookedInput Drop the prefix: users, input
Shadowing Reusing user in nested scope for a different concept Pick a more specific name for the inner scope (subscriber)
  1. What role does this value play in this scope? Not just “users” — “active users who get the newsletter.”
  2. Compare to the current name. Placeholder? Type-tagged? Boolean without predicate?
  3. Propose a name that describes the role. Re-read the next few lines that use it — do they become self-explanatory?

Smell Guidance
XInterface on a contract Name the contract (CategoryContract), or name the interface X and rename the class
Abstract* with one subclass Drop the prefix or inline
Static utility class with one method Fold onto domain object, or rename to the verb (Formatter)
Namespace doubles class name Drop redundant prefix: Engine\Runner, not Engine\EngineRunner
Trait named like a noun (UserTrait) Name the behavior: HasTimestamps, Sluggable
XManager / XService with CRUD Almost always a Repository or Store: UserRepository, OrderStore
getXxx / setXxx on pure data objects Consider readonly class (PHP 8.2+) or value object; drop get/set
Smell Guidance
IUser / TUser / UserType prefix on interface/type TypeScript has its own type/value namespace. Drop the prefix.
xxxCallback, xxxHandler for arrow functions Name what the callback does: onUserSelected, not selectionHandler
XService / XManager with CRUD XRepository, XStore, or XGateway
useXxx without hook semantics use* is reserved for React hooks. Don’t use it otherwise.
Default-exported class with generic name Prefer named exports. Name concretely: CategoryLineFormatter
File name doesn’t match primary export Rename file: parse-user-config.ts, not utils.ts
get* with no side effects Prefer getter or property: user.name, not user.getName()
Smell Guidance
XInterface suffix Go interfaces are small (1–3 methods). Name by the verb: Reader, Writer, Formatter
Get prefix on exported methods Idiomatic Go drops Get: Name(), not GetName()
Exported names with redundant package context user.NewUser()user.New(). The package is the context.
Impl suffix on implementations Name the concrete thing, not its relationship to an interface
Smell Guidance
XImpl suffix No. Name what it is, not what it implements
get_* prefix on field accessors Convention: drop get_ for simple accessors; keep for computation
Module+type repetition engine::Engine → prefer engine::Runner or rename module
*Error/*Kind suffix proliferation Group variants in the error type, not the name
Smell Guidance
XInterface/AbstractX Use Protocol or ABC; name by contract, not Interface
get_* / set_* methods Use @property where appropriate
Type-tagged variables in typed code user_list, user_dictusers, users_by_email
XManager / XService class XRepository, or fold methods onto domain objects

These are the signs you should stop and reconsider your candidate name, regardless of scope.

Symptom What it means
Method name repeats its class noun The class supplies the noun. Drop it.
Boolean without is/has/can/should Add the predicate.
Cycling through 4+ candidates and none fit The thing is doing too much. Propose a split.
AbstractX / BaseX with one implementation The qualifier isn’t earning its keep. Drop it.
Same noun for interface and its only implementation Pick which keeps the noun; the other gets a qualifier (or inline the interface).
Name longer than 4 words You’re describing, not naming. Find the noun.
Proposed rename without reading what the code does You’re guessing. Go back to diagnosis step 1.
Two equally-good candidates Pick the primary, note the alternative, move on.
Name reuses a term with a different meaning Vocabulary collision. Pick a different noun.
Name introduces a brand-new term without justification New term = new glossary entry. Justify it or reach for an existing term.
Picked the more abstract name because one method doesn’t fit The misfit belongs elsewhere. Flag it — don’t widen the name.

When conducting a naming review, produce two proposals per finding. The reviewer picks; your job is to make the choice informed.

  • Preservation — the smallest change that addresses the smell. Rename only. Leave structure, namespace, and surrounding code alone.
  • Aggressive — what you’d build with full ownership. May include splits, moves, folder restructuring, inlining, or extracting helpers.

If Preservation is also the best structural call, say so under Aggressive: “same as Preservation; no structural change indicated.” Don’t invent restructuring to fill the slot.

If the smell is purely structural and no rename helps, Preservation can be: “no rename — current name is the least bad option until the structural change in Aggressive lands.”

### `<currentName>`
[1–2 sentences: what it actually does, why the current name fails.]
- **Preservation** → rename to `<newName>`.

before: targets = process_data(subscribers) # app/newsletter/send.py:42 after: eligible = filter_active_with_email(subscribers)

- **Aggressive** → [rename + structural move, or "same as Preservation"]

For class/module scope, include a call-site survey artifact:

EngineHelper (app/Engine/Support/EngineHelper.php)
- Calculator.php:137 EngineHelper::getExecutableName() static
- Calculator.php:175 EngineHelper::extractEngineVersion($path) static; reader can't predict effect from "Helper"
- UploadedTaxReturn/Calculator.php:63 EngineHelper::extractEngineVersion(...) cross-namespace

Before presenting your analysis, verify:

  • Did you read the body / public surface, not just look at the name?
  • Did you quote at least one call site showing the failure or improvement?
  • Does any candidate contain a Tier 1 placeholder suffix?
  • For Tier 2 verb-er suffixes, did you write the one-line justification?
  • Did you grep the codebase for vocabulary collisions?
  • Did you produce both Preservation and Aggressive proposals?