RefactoringGENERALLANGUAGE-SPECIFICCONTESTED

Rename

The highest value-to-risk refactoring there is, and the most neglected. A better name is a better model, and the cost is usually one command.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

Why is renaming a thing worth a pull request of its own, when it changes no behaviour at all?

The requirement

A class called Manager handles subscription state transitions. New joiners take two days to work out what it does. Nobody renames it because "it is just a name" and it appears in 140 places.

The obvious build

Names are cosmetic. The code works; renaming touches 140 files, creates merge conflicts for everyone, and delivers nothing to a customer. Do it opportunistically, if ever.

Why it breaks

It treats the name as decoration rather than as the model. A name is the compressed statement of what a thing is, and a wrong one makes every reader build a wrong mental model before they read a line of the body (Naming).

How it breaks as requirements change
  • It treats the name as decoration rather than as the model. A name is the compressed statement of what a thing is, and a wrong one makes every reader build a wrong mental model before they read a line of the body (Naming).
  • The cost of a bad name is paid by every future reader, forever, and it is invisible in every estimate — the two days a new joiner spends are attributed to onboarding, not to the name.
  • It also compounds. Manager attracts unrelated responsibilities precisely because the name excludes nothing; a name that means anything cannot argue against a new method (God Object).
  • And the merge-conflict argument is backwards. A pure rename commit is the easiest possible conflict to resolve, and the pain is proportional to how long it is deferred, not to doing it (Review Size).
  • Meanwhile the divergence between the code's vocabulary and the business's vocabulary makes every requirement conversation a translation exercise, and translation is where requirements get lost (Naming and Domain Language).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The name appears in code, in two database column names, in an event payload consumed by another team, and in the runbook.
  • The codebase is typed and the IDE can perform the code-side rename mechanically.
  • One consumer is outside the repository, so the event field cannot be renamed in a single commit (Backward Compatibility as a Constraint).
Invariants
  • A rename changes no behaviour. If anything observable moved, the commit was not a rename.
  • The new name must be true of everything the thing does. Renaming Manager to SubscriptionStateMachine is only an improvement if it really is one (Naming).
  • Names in code, in data, in events and in conversation should converge over time, not diverge (Ubiquitous Language).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The name owns the whole contract: what the thing is, and by exclusion, what it is not.
  • A rename commit owns nothing else. No signature changes, no extracted methods, no "while I was in there" (What Refactoring Actually Is).
  • Whoever notices the name is wrong owns proposing the better one — the fix is usually a five-minute task and the noticing is the scarce part.
  • Names crossing a published boundary — event fields, API fields, column names — own a compatibility window rather than a rename (Expand and Contract).
Boundaries
  • Inside the repository, a rename is mechanical and reversible. Outside it — an event field, a public API, a column another service reads — it is a contract change and needs the migration treatment (Versioned Interfaces).
  • The boundary between a rename and a redesign: if the better name does not fit what the thing currently does, the finding is that the thing does too much, and renaming it is not the fix (Single Responsibility, Carefully).
  • Strings, reflection, serialized data and log-parsing dashboards are outside what the compiler checks. That set is the real risk surface of a rename.

What a name excludes

A good name does not just describe; it refuses. The practical value of SubscriptionLifecycle over SubscriptionManager is that the second one cannot argue with any method you propose to add, and the first one can.

That is why bad names and god objects appear together so reliably. The name came first (God Object).

The same class, two names, two years later
SubscriptionManager
class SubscriptionManager {
  activate(); cancel(); pause(); resume()
  sendRenewalEmail()      // added month 4
  exportToCsv()           // added month 9
  syncToSalesforce()      // added month 14
  recalculateMrr()        // added month 19
}

Every addition was defensible: it is
something you do with a subscription,
and the class "manages" subscriptions.
SubscriptionLifecycle
class SubscriptionLifecycle {
  activate(); cancel(); pause(); resume()
}

// "Where does sendRenewalEmail go?"
// "Not here — this is the lifecycle."

// The name did the arguing. Nobody had
// to have the responsibility conversation
// four separate times.

Both classes started identical. The difference is that the first name admits everything, so each of the four additions was locally reasonable and the review comment against them would have been an aesthetic one nobody wanted to make. The second name states a boundary, so the same four proposals meet a factual objection instead of a preference — and it costs nothing to make. This is naming acting as a structural force rather than as documentation (Single Responsibility, Carefully).

What the compiler does not rename

SIMPLIFIEDThe table treats each row independently, but the expensive real cases are combinations — a column name that is also an event field that also appears in three dashboards. In those cases the code-side rename is a five-minute task inside a two-week compatibility window, and the sequencing across the rows is the actual work (Data Migration).

The mechanical part of a rename is the safe part. The risk lives entirely in the references that are not code — string keys, column names, event fields, dashboards and documents — and the size of that set is what actually determines whether a rename is cheap.

The right-hand column is the discipline: a grep list, run before the commit, that covers each category the tooling could not see.

Where the old name appearsDoes the tooling rename it?Risk if missedWhat to do
Code identifiers, typed languageYes, mechanically verifiedNone — it will not compileLet the IDE do it and review the diff for surprises
Code identifiers, dynamic languageNo — text search onlyRuntime failure on an uncovered pathRename, then run the full suite, then grep for the string form as well (The Refactoring Loop)
String keys: feature flags, queue names, cache keysNoSilent behaviour change in production — the flag simply stops matchingGrep before committing; treat these as data, not names (Feature Flags and What They Cost)
Database column and table namesNoBroken queries, or a migration that locks a tableExpand and contract, never an in-place rename on a live table (Expand and Contract)
Event and API field namesNo — and consumers are outside the repoA consumer breaks after your deploy, not during itEmit both fields, migrate consumers, then remove (Versioned Interfaces)
Dashboards, alert rules, log queries, runbooksNoSilent loss of an alert, discovered during an incidentGrep the observability config too — this is the category teams forget (Debuggability by Design)

Renaming across a contract

Inside the repository a rename is one commit. Across a published contract it is a sequence, because there is a period during which both names must work — and skipping that period is how a rename becomes an incident.

The same sequence applies to a column, an event field, a public API field or a queue name. Only the length of the window changes.

Renaming an event field consumed by another team
  1. 1
    Rename in code only

    Rename the internal identifier everywhere the compiler can see, keeping the wire name unchanged via an explicit mapping.

    fails by Renaming the internal type and assuming the serializer follows, which changes the wire format in a commit that claimed to change nothing.

  2. 2
    Emit both names

    Write the new field alongside the old one, with identical values. Nothing breaks and nothing has to be coordinated yet (Expand and Contract).

    fails by Emitting the new field only in the happy path, so consumers see it inconsistently and cannot cut over.

  3. 3
    Announce and set a date

    Tell consumers the old field is deprecated, with a removal date and a way to check who is still reading it (Deprecation).

    fails by An announcement with no date, which means no consumer ever prioritises the migration.

  4. 4
    Observe the cutover

    Measure who still reads the old field — a metric on the field, or consumer confirmation. This is the step that makes removal safe rather than hopeful.

    fails by No measurement, so removal is a guess dressed as a schedule (Verify in Production in DevOps).

  5. 5
    Remove the old name

    Delete the old field, the mapping and the deprecation notice, in one small commit.

    fails by Never happening. The old field stays forever, and now both names are permanent — which is strictly worse than not having renamed at all (The Debt Register).

The last step is the one that is skipped, and skipping it converts a rename into a permanent duplication. If a team cannot commit to step five, the honest decision is not to start — a codebase with both subscriberId and customerId meaning the same thing is worse than one with a merely mediocre name.

How to build it

Most important first.

  • Take the name from the domain, not from the pattern catalogue. SubscriptionLifecycle says what it is; SubscriptionManager says only that it is a class (Ubiquitous Language).
  • Let the tooling do it. A typed language's rename is a mechanically verified transformation and is a fundamentally different risk from a find-and-replace (The Refactoring Loop).
  • Then grep for the old name in strings, config, SQL, dashboards, log queries and documentation, because none of those were included.
  • Ship it alone. A rename mixed with anything else destroys the property that makes it reviewable in ten seconds (Review Size).
  • For names crossing a contract, expand and contract: add the new name, write both, migrate consumers, then remove the old one (Expand and Contract).
  • Rename when you learn something. The moment you find yourself explaining what a thing "really" is, you have discovered the name it should have had, and that moment is when the rename is cheapest and best-informed (The Design Loop).

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • The rename itself: minutes with tooling, plus a grep, plus a ten-second review. This is the cheapest refactoring available and the ratio of value to cost is unmatched by anything else in this module.
  • The next change under a good name: whoever makes it starts from a correct model and does not add an unrelated method, because the name excludes it. That exclusion is the durable benefit and it compounds quietly.
  • The next change under Manager: a reader spends twenty minutes establishing what it does, and then adds a method that does not belong, because nothing in the name says it does not (God Object).
  • What does not get cheaper: a rename crossing a published event or column costs a full compatibility window — two deploys, a migration and a consumer chase — and that cost is not proportional to the size of the edit at all (Data Migration).
What the recommended approach costs
  • Renames create merge conflicts for anyone with an open branch, and on a busy file that is a genuine, if small and one-time, cost to other people.
  • A rename makes git blame and history search harder — the old name no longer finds the code, and tooling support for following renames is uneven.
  • Naming discussions can consume more time than they return. The failure mode of taking naming seriously is a team that argues about names in every review (Tone, Disagreement and Receiving Review).

What can go wrong

Failure modes
  • The new name is also wrong, and now there are two bad names in the history and a team more reluctant to try again.
  • The rename is bundled with a behaviour change, so a genuinely mechanical commit becomes an unreviewable one (What Refactoring Actually Is).
  • The string-keyed references are missed: a feature-flag key, a queue name, a dashboard query. Everything compiles and something in production silently stops matching.
  • Partial rename. Half the codebase says Manager and half says Lifecycle, and now readers must know both and that they are the same thing — worse than either name alone.
  • The mitigation fails: a team institutes a naming review, and the discussion cost per name exceeds the value of the names (Tone, Disagreement and Receiving Review).
Dependencies, and their direction
  • A rename inside a module depends on nothing and is close to free.
  • A rename crossing a module boundary depends on every importer, which the compiler handles in a typed language and nothing handles in an untyped one.
  • A rename crossing a published contract depends on consumers you may not control, and its cost is dominated entirely by the compatibility window rather than by the edit (Deprecation).
Misreads
  • "Names are subjective, so any name is as good as another." Names differ in what they exclude. Manager excludes nothing, which is exactly why unrelated responsibilities accumulate behind it (Single Responsibility, Carefully).
  • "Renaming is risky because it touches many files." Touching many files mechanically is much safer than touching a few by hand. Risk lives in the string-keyed references, and those are found with a grep (Review Size).
  • "We will rename it when we refactor it properly." The rename is the cheapest part and delivers most of the comprehension benefit. Coupling it to a larger project is how it never happens.
  • "A comment can explain the bad name." A comment is a second thing to keep true. The name is read every time and the comment is read once (Comments).
Smells this explains
  • god-object

Testing it, and how it ages

What to test, and at which boundary
  • Not a single test should change. If assertions moved, something other than a name moved.
  • The real test is the grep: old name in strings, SQL, config, dashboards, alert rules, runbooks. Those are the references the compiler cannot see (Stable Identifiers).
  • For a contract-crossing rename, a test that both the old and new field are present during the compatibility window, and a scheduled reminder to remove the old one (Expand and Contract).
How this design ages
  • Names should track understanding. The name you give something on day one is a hypothesis, and refusing to update it after learning what the thing actually is means the code permanently encodes your least-informed model (Choosing the Model).
  • A codebase where renames are routine converges on the business's vocabulary, which is what makes requirement conversations short. One where they are not diverges steadily, and the translation layer lives in people's heads (Naming and Domain Language).
  • What eventually forces it: onboarding. The cost of divergent naming is paid almost entirely by new people, which is why it is invisible to the team that created it.

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALThat a name is the model rather than a label holds everywhere; what varies enormously is the mechanical cost, which is near-zero with a type checker and substantial in a dynamic codebase where the same rename is a search across strings, serialized payloads and reflection.
  • LANGUAGE-SPECIFICIn a typed language with tooling, a code-side rename is verified by the compiler and is genuinely a one-command operation. In Python, Ruby or JavaScript the same rename is a text search that misses getattr, dictionary keys, ORM column mappings and serialized data — so the identical refactoring needs tests, a staged rollout and a grep discipline that the typed version does not.
  • CONTESTEDThe strongest opposing view is that renames impose real costs on everyone else — conflicts on open branches, broken git blame, broken muscle memory and broken external search results — for a benefit accruing mainly to future readers who do not yet exist, and that the churn of a team that renames enthusiastically outweighs the clarity. This is a serious position on large, long-lived, multi-team codebases. The distinction that resolves most of it is whether the name is *wrong* or merely not what you would have chosen: renaming a misleading name is close to always right, and renaming a merely different one is churn.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • Testing & Reliability Engineering — the string-keyed references a rename can miss are exactly the ones no test covers, which is why the grep discipline is a substitute for coverage rather than an addition to it.