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.

SuffixWhy it’s empty
*Service”Service” means everything and nothing
*ManagerManages what, how?
*HandlerHandles what, how?
*HelperA drawer of unrelated things
*Util / *UtilitySame — unrelated grab-bag
*SupportSupports what?
*ProcessorProcesses what, how?
*WrapperWraps 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[] {} }
SmellSignalFix
Placeholder className contains Tier 1 suffix (Helper, Manager, Service)Read the public surface; name what it actually does
Multiple responsibilitiesPublic methods fall into 2+ unrelated groups; honest summary needs “and”Don’t rename — split. Propose new classes with clear boundaries
Interface misnamedXInterface + class X — interface named after class instead of contractName the contract (CategoryContract) or name the interface X and rename the class
Mechanism in nameName 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 utilitySingle-method static class (StringHelper::format())Move the method onto a domain object, or rename to the verb (LineFormatter)
Wrong folderWell-named class in a folder that contradicts its purposeKeep the name, move the file. Group related concepts (Engine/{Binary, Runner})
Vocabulary collisionName reuses a term that already means something different in the codebasePick a different noun, or rename the existing user first
Brand-new termName introduces a term that appears nowhere else in the codebaseJustify in one line why no existing term fits, or reach for an existing term
Abstract/Base qualifierAbstractX or BaseX with only one concrete implementationDrop the prefix or inline the abstract class
Namespace repetitionEngine\EngineRunner, Calc\CalculatorServiceDrop redundant prefix: Engine\Runner
Misfit methodOne method doesn’t quite fit the proposed nameThe 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:

TypePlaceholders
Verb rootsprocess*, 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)
SmellSignalFix
Placeholder verbprocess(), handle(), manage(), do(), perform(), execute()Read the body; name what it actually does
Placeholder nounprocessData(), handleInfo() — generic verb + generic nounName the specific verb on the specific noun
Repeats class nounUser::getUserName(), Order::orderTotal()The class supplies the noun. Drop it: User::name(), Order::total()
Boolean without predicatevalid(), done(), loaded()Reads as a command. Fix: isValid(), hasFinished(), isLoaded()
Does two thingsHonest summary needs “and”: “filters records and projects to log entries”Split into filter* + project*
Callers disagree on returntargets = f(...), cleaned = f(...), entries = f(...) — different variable namesFunction name doesn’t tell callers what they got. Rename so they pick the same word
Misleading verbsave() that actually upserts; get() that creates if missingName the actual operation: upsert(), getOrCreate()
Vestigial get/setgetName() on a data class with no side effects, in a language with property syntaxDrop 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)
SmellSignalFix
Placeholderdata, info, temp, result, obj, item, arr, val, stuffName the role: activeUsers, dispatchLogEntries
Type-taggeduserObj, userArr, userMap, usersListThe type system already tells you the type. Drop the tag: user, users, usersByEmail
Boolean without predicatevalid, done, loadedisValid, hasFinished, isLoaded
Hungarian notationstrName, iCount, bActive in a typed languageDrop the prefix: name, count, isActive
Single-letterr["email"], r["status"] — throwaway name for a non-throwaway valueUse the domain noun: subscriber
Cardinality mismatchuser = users.filter(...) returning a listMatch the type: activeUsers for array, firstActiveUser for single
Vestigial qualifierallUsers when there’s no someUsers; rawInput when there’s no cookedInputDrop the prefix: users, input
ShadowingReusing user in nested scope for a different conceptPick 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?

SmellGuidance
XInterface on a contractName the contract (CategoryContract), or name the interface X and rename the class
Abstract* with one subclassDrop the prefix or inline
Static utility class with one methodFold onto domain object, or rename to the verb (Formatter)
Namespace doubles class nameDrop redundant prefix: Engine\Runner, not Engine\EngineRunner
Trait named like a noun (UserTrait)Name the behavior: HasTimestamps, Sluggable
XManager / XService with CRUDAlmost always a Repository or Store: UserRepository, OrderStore
getXxx / setXxx on pure data objectsConsider readonly class (PHP 8.2+) or value object; drop get/set
SmellGuidance
IUser / TUser / UserType prefix on interface/typeTypeScript has its own type/value namespace. Drop the prefix.
xxxCallback, xxxHandler for arrow functionsName what the callback does: onUserSelected, not selectionHandler
XService / XManager with CRUDXRepository, XStore, or XGateway
useXxx without hook semanticsuse* is reserved for React hooks. Don’t use it otherwise.
Default-exported class with generic namePrefer named exports. Name concretely: CategoryLineFormatter
File name doesn’t match primary exportRename file: parse-user-config.ts, not utils.ts
get* with no side effectsPrefer getter or property: user.name, not user.getName()
SmellGuidance
XInterface suffixGo interfaces are small (1–3 methods). Name by the verb: Reader, Writer, Formatter
Get prefix on exported methodsIdiomatic Go drops Get: Name(), not GetName()
Exported names with redundant package contextuser.NewUser()user.New(). The package is the context.
Impl suffix on implementationsName the concrete thing, not its relationship to an interface
SmellGuidance
XImpl suffixNo. Name what it is, not what it implements
get_* prefix on field accessorsConvention: drop get_ for simple accessors; keep for computation
Module+type repetitionengine::Engine → prefer engine::Runner or rename module
*Error/*Kind suffix proliferationGroup variants in the error type, not the name
SmellGuidance
XInterface/AbstractXUse Protocol or ABC; name by contract, not Interface
get_* / set_* methodsUse @property where appropriate
Type-tagged variables in typed codeuser_list, user_dictusers, users_by_email
XManager / XService classXRepository, or fold methods onto domain objects

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

SymptomWhat it means
Method name repeats its class nounThe class supplies the noun. Drop it.
Boolean without is/has/can/shouldAdd the predicate.
Cycling through 4+ candidates and none fitThe thing is doing too much. Propose a split.
AbstractX / BaseX with one implementationThe qualifier isn’t earning its keep. Drop it.
Same noun for interface and its only implementationPick which keeps the noun; the other gets a qualifier (or inline the interface).
Name longer than 4 wordsYou’re describing, not naming. Find the noun.
Proposed rename without reading what the code doesYou’re guessing. Go back to diagnosis step 1.
Two equally-good candidatesPick the primary, note the alternative, move on.
Name reuses a term with a different meaningVocabulary collision. Pick a different noun.
Name introduces a brand-new term without justificationNew term = new glossary entry. Justify it or reach for an existing term.
Picked the more abstract name because one method doesn’t fitThe 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?