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.
Core Principles
Section titled “Core Principles”Before diagnosing specific smells, internalize these principles. They apply to every identifier you write or review.
1. Predictability
Section titled “1. Predictability”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.
2. One Word, One Concept
Section titled “2. One Word, One Concept”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.
3. Domain Noun Beats Role Label
Section titled “3. Domain Noun Beats Role Label”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.
5. The Namespace Is Half the Name
Section titled “5. The Namespace Is Half the Name”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 repetitionfrom engine.calc import EngineCalculator
# Good: namespace provides contextfrom engine.calc import Calculator6. Read It Where It’s Used
Section titled “6. Read It Where It’s Used”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.
7. Symmetry Across Siblings
Section titled “7. Symmetry Across Siblings”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.”
8. Length Proportional to Blast Radius
Section titled “8. Length Proportional to Blast Radius”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.
9. No Qualifier Without Contrast
Section titled “9. No Qualifier Without Contrast”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.
Suffix Discipline
Section titled “Suffix Discipline”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:
- Single-purpose verb. The class does one verb on one kind of thing.
- Concrete domain prefix.
EngineRunner✓.DataRunner✗ (“data” isn’t a thing). - Can’t fold onto the domain object. If
format()belongs on the thing being formatted, don’t make a separateFormatter.
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 EngineBinaryclass EngineRunner { run(binary: EngineBinary): Result {} }
// Tier 2 — justified: single verb, concrete prefixclass CategoryLineFormatter { format(lines: string[]): string {} }
// Tier 1 — rejected: "Processor" says nothingclass DataProcessor { process(data: any): any {} }// Fix: find the noun. What does it actually do?class SubscriberFilter { filterActiveWithEmail(records: Subscriber[]): Subscriber[] {} }Smell Catalog — Classes & Modules
Section titled “Smell Catalog — Classes & Modules”| 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 |
Diagnosis Procedure
Section titled “Diagnosis Procedure”- List the public surface. What methods/properties does this class expose?
- State what it does in one sentence. Use only verbs/nouns from the surface. If you need
stuff,things, orhelpers, the current name doesn’t match. - Compare to the current name. Note the gap.
- 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. - 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
- Caller picks a compensating variable name (
- Propose candidates. 1–3 names, each with one line of rationale tied to behavior.
- 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.
- Replay at every call site. Mentally substitute: does every call site read naturally without compensating variable names or comments?
- If no candidate is self-contained — the class is doing too much. Propose a split, don’t pick a vaguer name.
Part 2: Function / Method Naming
Section titled “Part 2: Function / Method Naming”Functions and methods wear the verb. A bad verb forces every caller to read the body to understand what happens.
Suffix Discipline — Placeholder Verbs
Section titled “Suffix Discipline — Placeholder Verbs”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 nountargets = process_data(subscribers) # what happened to subscribers?
# Good: specific verb on specific nouneligible = filter_active_with_email(subscribers)Smell Catalog — Functions & Methods
Section titled “Smell Catalog — Functions & Methods”| 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) |
Diagnosis Procedure
Section titled “Diagnosis Procedure”- Read the body. State what it does in one sentence, using verbs/nouns from the body.
- Compare to the current name. Note the gap. Apply Suffix Discipline.
- Survey call sites. What variable name do callers pick for the return value? Do different callers pick different names?
- Propose candidates. 1–2 names. Replay: does
result = candidate(args)predict the body? - 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
Part 3: Variable / Parameter Naming
Section titled “Part 3: Variable / Parameter Naming”Variables live within one function — the smallest scope, but also the most frequent. A bad variable name makes every line that uses it ambiguous.
Suffix Discipline — Placeholder Nouns
Section titled “Suffix Discipline — Placeholder Nouns”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: placeholderdata = filter(...)await send_newsletter(data) # what is data?
# Good: role-namedrecipients = filter(...)await send_newsletter(recipients)Smell Catalog — Variables & Parameters
Section titled “Smell Catalog — Variables & Parameters”| 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) |
Diagnosis Procedure
Section titled “Diagnosis Procedure”- What role does this value play in this scope? Not just “users” — “active users who get the newsletter.”
- Compare to the current name. Placeholder? Type-tagged? Boolean without predicate?
- Propose a name that describes the role. Re-read the next few lines that use it — do they become self-explanatory?
Part 4: Language-Specific Idioms
Section titled “Part 4: Language-Specific Idioms”| 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 |
JavaScript / TypeScript
Section titled “JavaScript / TypeScript”| 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 |
Python
Section titled “Python”| 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_dict → users, users_by_email |
XManager / XService class | XRepository, or fold methods onto domain objects |
Part 5: Red Flags Summary
Section titled “Part 5: Red Flags Summary”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. |
Part 6: Review Output Format
Section titled “Part 6: Review Output Format”When conducting a naming review, produce two proposals per finding. The reviewer picks; your job is to make the choice informed.
Preservation vs. Aggressive
Section titled “Preservation vs. Aggressive”- 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.”
Output Template
Section titled “Output Template”### `<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-namespacePre-Output Self-Check
Section titled “Pre-Output Self-Check”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?