A refactor is a code change that lowers the cognitive cost a program adds beyond what its problem requires, while behavior and delivered capability stay fixed. That surplus cost is accidental complexity.
A refactor is therefore a maintainability move, not a verification method: it reduces no uncertainty about whether the code is correct. Testing, type-checking, and formal verification are its complement.
Essential and accidental complexity¶
The distinction comes from Brooks, who divided software's difficulties into essence and accidents (Brooks 1987)1. Some complexity is essential: the concepts a capability genuinely requires, irreducible for as long as the capability is kept. The rest is accidental: extra cognitive cost from how the code happens to be written, removable without giving anything up.
Real code sits above the floor because features arrive faster than cleanup, and each one piles on some accidental cost.
The move: hold capability, lower cost¶
Reading "capability" as features only is the common trap. Speed, safety, and verifiability are capabilities too, so a structure that exists to buy performance or a safety guarantee is essential to that capability. Deleting it because it looks like clutter is a capability loss, not a refactor.
A refactor moves toward a shape once duplication and structure reveal it, rather than imposing an abstraction onto code that hasn't earned it. Collapsing two blocks that only resemble each other is the classic misfire: the wrong abstraction couples them under a concept the domain never had, and costs more than the duplication it replaced (Metz 2016)2. That prescriptive reflex adds accidental complexity rather than removing it: it is design folklore, not refactoring.
What it catches¶
Accidental complexity accumulates in a few recurring forms:
- Dead code — unreachable branches, unused functions, exports nothing imports. Pure accidental cost: removing it loses no capability, and the "is this still used?" load goes with it.
- Divergent duplication — the same logic copied and drifted apart, so the reader has to hold every copy at once. The hardest case is duplication that emerged independently, where the shared concept was never named.
- Unrecognizable structure — code that matches no idiom or convention the reader already holds, forcing re-derivation where recall would have done.
- Unpredictable layout — like things shaped differently, so their location can't be guessed and every lookup becomes a search.
All of these forms block new work: a developer cannot add the next feature because the code around it is unreadable.
Refactoring does not touch what isn't accidental: missing requirements, or performance rooted in algorithmic choice rather than code shape. Those are essential-complexity questions a reshape cannot reach.
The discipline, and what actually stays fixed¶
Three practices separate refactoring from rewriting:
- Tests exist before the refactor starts. Without them, the change is a rewrite and behavior preservation rests on untested judgment. For legacy code with none, the first move is to get its current behavior under test, whatever that behavior is, before touching the code.
- Each step is small and reversible. A session is many tiny commits, each green: one change, one green run, and a revert when the run fails.
- Behavior is preserved by construction. Some moves are equivalence-preserving transforms: a rename that collides with no existing name, or a codemod whose rewrite rule preserves equivalence, produces equivalent code by mechanical rewrite. Tests are the net; the transform is the proof.
What stays fixed is behavior at the boundary, not the whole test suite. Behavior is defined where the piece meets the outside, so only tests written at that boundary survive an internal reshape. A test pinned to internal structure describes the structure, so any move that reshapes it breaks the test, and that breakage reads as a behavior change even though the boundary behavior is untouched. Such tests are scaffolding: they die with the structure they described, and their loss is not a regression. The more internal the test, the smaller the move it permits: over-specified unit tests give confidence now and later obstruct the cleanups they were meant to protect.
Tools¶
Refactoring tooling does two separate jobs: deciding where the accidental cost sits, and making the change without altering behavior. The first is semantic judgment; the mechanical tools only guarantee the change is safe, never that it was worth making.
Finding where the cost sits¶
- AI code review. Agents such as Claude's code-review skill and Cursor (both commercial) read the diff semantically and flag the accumulated complexity, naming, and duplication that motivate a refactor.
- Git-history hotspots. Files that change often and attract bug-fix commits are the empirical signal for where to spend the effort.
Making the move without changing behavior¶
- Codemods encode a mechanical move as a reusable, reviewable script that runs across a whole codebase: jscodeshift / ts-morph (JS/TS), comby (language-agnostic structural search and replace), rope (Python), and OpenRewrite (Java/Kotlin, recipe-based).
- A formatter is not a refactoring, but running one consistently (gofmt, Prettier, Black, rustfmt) makes moves stable in the stable-sort sense: unchanged code keeps its place, so a reshape lands as only the behavior-relevant diff instead of a mass of formatting changes. That clean diff is what keeps a move small, reviewable, and its effect isolable.
- A regression net — approval and snapshot tests pin current behavior so a reshape that changes it shows up; they catch change, not correctness.
- A net is only worth trusting if it is strong enough to catch the behavior changes a bad refactor introduces, which is what measuring test-suite effectiveness establishes.
When to use, when not¶
Use:
- Before adding a feature to a hard-to-change area: make the change easy first, then make the easy change.
- When tests exist and the code is the bottleneck. Extract, rename, simplify; the boundary tests flag a move that changes the behavior they cover.
- As a routine PR-time activity. Small refactors compound: each PR that leaves a file a little closer to its floor lowers the cost of the next change there.
Don't:
- Without tests. There is no boundary to hold the reshape against, so it is rewriting, not refactoring.
- For green-field code where the tests don't exist yet. The shape is still being found, and refactoring pays once that shape stops fitting.
- As a substitute for a real redesign. When the essential floor itself is wrong (the wrong concepts, the wrong architecture), no down-move reaches it; reshaping only locks in the wrong frame. That case calls for a budgeted rewrite, with the decision recorded as an ADR.
- For its own sake. A refactor without a destination is re-arrangement; the move worth making is the one that unblocks the next change.
Evidence¶
- How programmers actually refactor. Commit messages do not reliably indicate refactoring, and refactoring tools are underused, particularly for refactorings at method granularity and above (Murphy-Hill et al. 2009)3.
- A methodology caveat. Version-control snapshots are an imprecise, incomplete record of how code evolves (Negara et al. 2012)4. That is why Negara and colleagues mine 1,520 hours of changes recorded inside the IDE from 23 developers (Negara et al. 2014)5.
The case for refactoring rests on maintainability: code that cannot be safely changed loses value over time. Both studies describe what programmers do, not whether refactoring improves maintainability, and that improvement is hard to measure because the essential floor cannot be located in practice. The capability-vs-cost sketch is a mental model, not a measured result.
The term, and other definitions¶
William Opdyke defined refactoring as a behavior-preserving program transformation: a restructuring guarded by preconditions that guarantee observable behavior is unchanged (Opdyke 1992)6. The definition most developers use is Martin Fowler's: a change to the internal structure of software that makes it easier to understand and cheaper to modify without altering observable behavior (Fowler 1999)7.
Both define refactoring by what it may change. The definition used here defines it by purpose: reducing accidental complexity.
Classification¶
- Quality dimensions: Maintainability.
- Area: Maintenance of long-lived codebases, legacy code without tests, structural cleanup ahead of new features.
Referenced by¶
- Quality dimensions · Quality dimensions
- How AI fits into software quality · AI
- Design folklore · Conventional
References¶
-
Brooks, Frederick P. 1987. "No Silver Bullet: Essence and Accidents of Software Engineering." Computer 20 (4): 10–19. https://doi.org/10.1109/MC.1987.1663532. ↩
-
Metz, Sandi. 2016. The Wrong Abstraction. Sandi Metz's blog. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction. ↩
-
Murphy-Hill, Emerson, Chris Parnin, and Andrew P. Black. 2009. "How We Refactor, and How We Know It." Proceedings of the 31st International Conference on Software Engineering (ICSE '09), 287–97. https://doi.org/10.1109/ICSE.2009.5070529. ↩
-
Negara, Stas, Mohsen Vakilian, Nicholas Chen, Ralph E. Johnson, and Danny Dig. 2012. "Is It Dangerous to Use Version Control Histories to Study Source Code Evolution?" ECOOP 2012 – Object-Oriented Programming: 26th European Conference. https://doi.org/10.1007/978-3-642-31057-7_5. ↩
-
Negara, Stas, Mihai Codoban, Danny Dig, and Ralph E. Johnson. 2014. "Mining Fine-Grained Code Changes to Detect Unknown Change Patterns." Proceedings of the 36th International Conference on Software Engineering (ICSE '14), 803–13. https://doi.org/10.1145/2568225.2568317. ↩
-
Opdyke, William F. 1992. "Refactoring Object-Oriented Frameworks." PhD thesis, University of Illinois at Urbana-Champaign. https://www.laputan.org/pub/papers/opdyke-thesis.pdf. ↩
-
Fowler, Martin. 1999. Refactoring: Improving the Design of Existing Code. Addison-Wesley Professional. https://archive.org/details/isbn_9780201485677. ↩