What Refactoring Actually Is
Changing internal structure without intentionally changing observable behaviour. Almost everything called a refactor is something else, and the difference is what makes it safe.
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.
What separates a refactor from a rewrite, and why does the distinction decide how the work should be done?
A ticket says "refactor the billing module". Reading it, the work includes renaming three classes, splitting a service, fixing a rounding bug, and changing how proration is calculated. It is estimated as one task.
Refactoring means improving code. The billing module is bad, so improving it — cleaner names, better structure, and fixing the bugs you find on the way — is all one activity, and separating it into different tickets is bureaucratic.
It destroys the safety argument. The reason a refactor is low-risk is that you can compare before and after and demand they be identical; mix in a behaviour change and there is no longer anything to compare against (Characterization Tests).
- It destroys the safety argument. The reason a refactor is low-risk is that you can compare before and after and demand they be identical; mix in a behaviour change and there is no longer anything to compare against (Characterization Tests).
- It makes review impossible. Six hundred lines of moved code with four changed lines inside them is not reviewable by any human, and the four lines are the only ones that can break production (Review Size).
- It makes bisecting useless. When invoices come out wrong three weeks later, "refactor billing" is a commit that changed everything and could have changed anything.
- It removes the ability to stop. A structural improvement can be abandoned halfway with no harm; a half-finished proration change cannot, so the work stops being interruptible exactly when the deadline arrives.
- And the word itself stops carrying information. Once "refactor" can mean anything from a rename to a rewrite, "I refactored it" tells a reviewer nothing about the risk they are being asked to assess (A Review Checklist Worth Reading).
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.
- Billing is in production and invoices are already out; any behaviour change is visible to customers and to finance.
- The team wants to ship weekly, so the work has to be interruptible at any point.
- The test suite covers the happy path and almost nothing else.
- The rounding bug is real and someone has to fix it — the constraint is not that behaviour must never change, it is that changing it must be a separate, visible decision.
- While refactoring, observable behaviour does not change. Same inputs, same outputs, same side effects, same errors.
- Every commit leaves the system deployable, because a refactor that must be finished to be correct is a rewrite in instalments.
- A behaviour change is always its own commit, with its own test, and says so.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- The refactoring commits own structure and nothing else: names, boundaries, the shape of the code. Their test suite must not change.
- The bug fix owns exactly one behaviour change, with a test that fails before it and passes after.
- The proration change owns a requirement — it is a feature, not a cleanup, and it belongs in a ticket that says what the new behaviour is.
- Whoever writes the ticket owns not calling all three of these one thing.
- The line is observable behaviour: what a caller, a user, a downstream system or a log consumer can see. Inside that line you may move anything; across it you are changing the product.
- "Intentionally" is doing real work in the definition. A refactor that accidentally changes behaviour is a defect, not a redefinition — and the whole practice exists to make those accidents detectable (The Refactoring Loop).
- Performance sits awkwardly on the line. Nobody calls a 5% slowdown a behaviour change; a 50× slowdown is one, because something downstream will time out (Designing for Cost).
One ticket, three activities
The billing ticket is not one piece of work. It contains a behaviour-preserving restructure, a bug fix and a feature, and each has a different risk profile, a different review, and a different way of being wrong.
The point of separating them is not tidiness. It is that only the first one has a safety argument, and mixing it with the others silently withdraws that argument from the whole.
commit 3f1a9c "refactor billing module" 47 files changed, 1,340 insertions, 890 deletions - renamed BillingService -> InvoiceCalculator - split proration out of the service - fixed rounding (was floor, now half-up) - proration now excludes the trial period - updated 14 test files Invoices are wrong three weeks later. Bisect points here. So does everything.
1. "rename BillingService -> InvoiceCalculator" tests: unchanged 2. "extract Proration from InvoiceCalculator" tests: unchanged 3. "fix: round half-up, not floor (#4412)" tests: one new failing-then-passing case 4. "proration excludes trial period (#4501)" tests: three new cases; requirement in the ticket Bisect points at 3 or 4. Both are small.
Commits 1 and 2 have a property that 3 and 4 do not: their test suite is unchanged, so "did behaviour move" is a question with a mechanical answer. Bundling all four withdraws that property from the whole change — there is no longer any subset of the diff that can be checked by comparison, so the only available verification is reading 1,340 lines, which nobody does. The separation costs three extra reviews and buys the ability to answer "what broke this" in ninety seconds.
Which activity is this, actually?
Before starting work on existing code, it is worth deciding which of these you are doing, because they need different preparation and carry different risk. Most of the trouble in this module comes from starting one and finishing another.
- The first four differ in what verification is available, not in how much care they deserve.
- A single commit that spans two of these categories inherits the weaker verification of both.
- The most expensive mistake in this list is starting at the top and ending at the bottom without anyone deciding to (Refactor or Rewrite).
Does observable behaviour change, and is that change intended?
when Structure changes, behaviour does not, and you can check that.
cost Needs a way to detect behaviour change before you start. Interruptible at any commit; low risk when done in small steps; buys nothing on its own — the return comes from the change it makes cheaper (The Cost of Change).
when Behaviour changes because the current behaviour is wrong.
cost Needs a failing test first. Usually small. The risk is that something downstream depended on the bug, which is more common in billing and reporting than anyone expects.
when Behaviour changes because someone asked for different behaviour.
cost Needs a requirement, a stated invariant and a migration story if data or contracts move (Designing a Feature Before Writing It). Not a cleanup, and calling it one loses the requirement.
when No behaviour-preserving path reaches the structure you want.
cost Needs a migration argument, a way back, and an honest estimate of the behaviour nobody has written down (The Risk in a Rewrite). Legitimate sometimes; never justified on the grounds that the result would be cleaner.
when Behaviour is unchanged; timing and resource use are not.
cost Sits on the boundary. Needs a measurement before and after, and an explicit view on whether the timing change is observable to anything downstream (Premature Optimization, Reclaimed).
How the word gets lost
The misuse is not carelessness about vocabulary. Each of these has a reason people do it, and the reason is usually good — which is why the failure recurs on teams that know the definition perfectly well.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A bug is found mid-refactor | The fix goes into the structural commit | Fixing it now is genuinely cheaper than a context switch, and it feels wrong to leave known-broken code | Note it, finish the structural step, fix it next — with its own failing test. If it is urgent, stop the refactor and fix it first, cleanly. |
| A cleanup ticket with no named purpose | Weeks of restructuring, no requirement gets easier | Structure with no named change it makes cheaper is aesthetics with a schedule | Name the change it makes cheaper before starting. If none can be named, the honest answer is not to do it (Changeability Is the Goal). |
| The new structure is half-built | Nothing is deployable and stopping is not an option | The steps were too large, so the system spends days in an intermediate state | Smaller steps, each one leaving the system working — which usually means the old and new shapes coexist for a while (Expand and Contract). |
| A rewrite is proposed as a refactor | "We will refactor it incrementally" with no incremental step ever described | The word carries a safety connotation that the plan does not deserve | Ask for the first three steps and what is deployable after each. If they cannot be described, it is a rewrite (The Strangler Pattern). |
| Tests are edited during a refactor | Assertions change, suite stays green | The tests were coupled to structure rather than behaviour, so moving the structure broke them | Treat it as a finding about the tests. Rewriting assertions to match new behaviour is a behaviour change wearing a refactor's commit message (What a Unit Is). |
How to build it
Most important first.
- Split the ticket. Three activities, three sets of commits, three risk profiles: rename and split (behaviour-preserving), fix rounding (one behaviour change, one test), change proration (a feature with a requirement).
- Do the behaviour-preserving work first where you can, because it usually makes the behaviour change small — the proration edit is four lines once proration has somewhere to live (Extract Module).
- Capture current behaviour before touching anything you cannot verify. In billing, "current behaviour" includes the bug, and pinning the bug on purpose is correct — you remove it deliberately, later, in its own commit (Characterization Tests).
- Keep every commit deployable. If the refactor has to be finished before the system works, it is a rewrite and needs the rewrite's argument about migration (The Risk in a Rewrite).
- Say which kind of change each commit is, in the message. This is nearly free and it is what makes the history usable a year later.
- When a refactor uncovers a bug, write it down and keep going. Fixing it inside the refactor is the single most common way this discipline is lost.
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.
- Under one-big-refactor-ticket: the next change to billing costs the same as before, because the structural work was never separable from the behaviour work and neither got finished properly. If it broke, the next change also costs an incident investigation against a commit that changed everything.
- Under split-commits: the next change costs whatever the new structure makes it cost, and — critically — you can tell. A regression bisects to a four-line commit rather than a six-hundred-line one.
- The rounding fix, once separated, costs one test and one line, forever. Bundled, it costs a share of the risk of the whole ticket.
- What does not get cheaper: splitting the work means three reviews instead of one and more calendar time. The discipline buys attributability and interruptibility, not speed (Review Size).
- Strict separation costs commits, reviews and calendar time, and on a two-person team with high trust some of that ceremony genuinely returns nothing.
- Doing behaviour-preserving work first means touching code twice, and occasionally the restructuring turns out to have been for a change that was then cancelled.
- Refactoring is a change to working code. Every one carries a nonzero chance of breaking something that worked, and a codebase that is never restructured is never broken by restructuring (When Design Does Not Pay).
What can go wrong
- The rewrite wearing the word. "Refactor the billing module" turns into a parallel implementation, and the safety argument that justified starting was never true (Refactor or Rewrite).
- Accidental behaviour change, undetected. The suite is green because the suite never covered the case, and the invoice is wrong for a month.
- The refactor that cannot stop: three days in, the old structure is gone and the new one is not finished, and the team is now committed regardless of what they learn.
- The mitigation fails too. Writing characterization tests first can pin behaviour so precisely that the tests block the change you actually wanted, and then someone deletes them (Characterization Tests).
- Refactoring as avoidance: the code gets restructured every quarter and the requirement everyone is afraid of never gets implemented.
- Refactoring depends on being able to detect a behaviour change, which means it depends on tests, or on characterization tests you write first, or on a comparison harness. Without one of the three it is unverified restructuring (Refactoring Without Tests).
- It depends on the tests being about behaviour rather than structure. A suite that asserts which methods were called blocks the refactor it was supposed to protect (Mocking).
- It depends on small commits reaching main frequently; a refactor on a three-week branch collides with everything (Trunk-Based Development in DevOps).
- "So refactoring is safe." It is *verifiable*, which is different. The safety comes from being able to check that behaviour did not move, and that check requires tests you may not have (Refactoring Without Tests).
- "Any improvement to code is a refactor." Only if observable behaviour is unchanged. Fixing a bug improves the code and is not a refactor, and calling it one is how bug fixes ship unreviewed inside structural diffs.
- "Refactoring means making code clean." It means changing structure for a purpose — usually so a specific upcoming change is cheaper. Structure with no named purpose is redecoration (Changeability Is the Goal).
- "You cannot refactor without tests." You can, carefully, in provably-safe steps, and sometimes you must. What you cannot do is refactor without *some* way to detect a behaviour change (Refactoring Without Tests).
Testing it, and how it ages
- The defining test of a refactoring commit: the test suite does not change. If assertions moved, behaviour probably moved with them, and the burden of proof is on the author.
- Where the suite is thin, write characterization tests first — recording what the code does now, including things you believe are wrong (Characterization Tests).
- For behaviour changes, one test that fails before and passes after. That test is the difference between a fix and a hope.
- For risky equivalence, run both implementations against real inputs and diff the outputs. This is what makes a large behaviour-preserving move verifiable at all (The Legacy Change Loop).
- Refactoring stops being separable when the structure is so far from the requirement that no behaviour-preserving path reaches it. That is the point where the rewrite conversation is legitimate, and it needs a migration argument rather than an aesthetic one (The Risk in a Rewrite).
- As tooling improves, more refactorings become mechanical and provably safe — a rename in a typed language is a different risk class from a rename by search and replace. The set of moves you can make without a test grows slowly (Rename).
- As a codebase acquires real test coverage, the cost of refactoring falls sharply, which is the strongest practical argument for coverage that this domain makes.
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.
- GENERALThe behaviour-preserving criterion is independent of language and paradigm; what varies is how much a compiler can guarantee for you, which changes the size of step that is safe rather than the definition itself.
- LANGUAGE-SPECIFICIn a statically typed language with good tooling, a rename or a signature change is mechanically verified and effectively free. In a dynamic language the same move is a search across strings, reflection and serialized data, so the identical refactoring belongs in a different risk class and needs tests that the typed version does not.
- CONTESTEDThe strongest opposing view is that this definition is too strict to be useful in practice: real cleanup work almost always uncovers bugs, and forcing them into separate commits means either leaving known-broken code or paying a context-switch cost on every one. Practitioners who work this way argue that a small, well-tested change that both restructures and fixes is more honest than a fiction of purity. The counter is bisectability — the value of the separation shows up months later, when something is wrong and the history has to answer for it.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — the entire safety argument for refactoring is a claim about detection, and how much detection a suite actually provides is that domain's question.