# Refactoring practice

A refactor reduces **accidental complexity**: the cognitive cost a program adds
beyond what its problem requires. Behavior and delivered capability stay fixed.

That makes it 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 frame here is Brooks's distinction (Brooks 1987)[^brooks1987]. Some complexity is
**essential**: the concepts a capability genuinely requires, irreducible for as
long as you want that capability. The rest is **accidental**: extra cognitive
cost from how the code happens to be written, removable without giving anything
up. Refactoring targets the accidental part.

<figure class="sketch" markdown>

![Sketch: capability on the x-axis, cognitive cost on the y-axis. A rising bold line is the essential-complexity floor; the shaded region above it is accidental complexity. A point high in that region marks where real code sits, and a downward arrow labeled "refactoring" drops it straight down toward the floor.](https://quality.stereobooster.com/assets/capability-complexity-light.svg#only-light)

<figcaption>
Capability on the horizontal axis, cognitive cost on the vertical. The bold
line is the essential-complexity floor; everything above it is accidental.
Refactoring is the downward pull at fixed capability. This is a schematic: the
axes carry no units and the floor cannot be located in practice, so only the
<em>direction</em> of a move means anything, never a coordinate.
</figcaption>

</figure>

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

Every refactoring holds capability fixed and lowers 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 dressed as a cleanup, 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. That prescriptive reflex adds accidental
complexity rather than removing it: it is
[design folklore](https://quality.stereobooster.com/design-folklore.md), not refactoring.

## What it catches

Accidental complexity accumulates in a few recurring forms:

- **[Dead code](https://quality.stereobooster.com/dead-code-detection.md)** — 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](https://quality.stereobooster.com/clone-detection.md)** — 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:

1. **Tests exist before the refactor starts.** Without them, you are rewriting
   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.
2. **Each step is small and reversible.** A session is many tiny commits, each
   green: one change, one green run, revert if it isn't.
3. **Behavior is preserved by construction.** Some moves are
   equivalence-preserving transforms: a consistent rename, or a codemod applied uniformly,
   produces equivalent code by mechanical rewrite regardless of who runs it.
   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: expect them to die with the structure
they described, and do not count their loss as a regression. The more internal
the test, the smaller the move it permits: over-specified unit tests buy
confidence today and charge it back as friction against the very 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](https://quality.stereobooster.com/code-review.md).** Claude's code-review skill, Cursor, and
  similar agents read the diff semantically and flag the accumulated-complexity,
  naming, and duplication that motivate a refactor.
- **[Git-history hotspots](https://quality.stereobooster.com/git-hotspots.md).** 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

The mechanical part of a down-move preserves behavior by construction; the work
is in choosing the move, not performing it.

- **Codemods** encode a mechanical move as a reusable, reviewable script that runs
  across a whole codebase: **[jscodeshift](https://github.com/facebook/jscodeshift)** / **[ts-morph](https://github.com/dsherret/ts-morph)** (JS/TS),
  **[comby](https://comby.dev/)** (language-agnostic structural search and replace),
  **[rope](https://github.com/python-rope/rope)** (Python), and **[OpenRewrite](https://docs.openrewrite.org/)**
  (Java/Kotlin, recipe-based).
- **A formatter is not a refactoring**, but running one consistently
  (**[gofmt](https://pkg.go.dev/cmd/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 reformatting storm. That clean diff is what
  keeps a move small, reviewable, and its effect isolable.
- **A regression net** — [approval and snapshot tests](https://quality.stereobooster.com/snapshot-testing.md)
  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](https://quality.stereobooster.com/measuring-test-effectiveness.md)
  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 prove behavior is preserved.
- As a routine PR-time activity. Small refactors compound; leave each file a
  little closer to its floor than you found it.

**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. Build the shape
  iteratively; refactor when the 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. See [ADRs](https://quality.stereobooster.com/adrs.md) for the decision
  shape and budget a rewrite.
- For its own sake. A refactor without a destination is re-arrangement; pick the
  move that unblocks the next thing.

## Evidence

- **How programmers actually refactor.** Programmers refactor far more than they
  self-report, and automated refactoring tools are under-used relative to their
  availability (Murphy-Hill et al. 2009)[^murphyhill2009].
- **A methodology caveat.** Around a tenth of code changes in real history are
  refactorings invisible at the commit level (Negara et al. 2014)[^negara2014].

The case for refactoring rests on maintainability: code that cannot be safely
changed loses value over time. The empirical literature is largely descriptive
(what programmers do, what tools support) rather than controlled outcome studies
that show refactoring improves it. The capability-vs-cost sketch is a mental
model, not a measured result.

## The term, and other definitions

"Refactoring" was coined by William Opdyke and Ralph Johnson, who defined it as a
**behavior-preserving program transformation**: a restructuring guarded by
preconditions that guarantee observable behavior is unchanged (Opdyke 1992)[^opdyke1992]. 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)[^fowler1999].

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](https://quality.stereobooster.com/quality-dimensions.md) · Quality dimensions
- [How AI fits into software quality](https://quality.stereobooster.com/ai.md) · AI
- [Design folklore](https://quality.stereobooster.com/design-folklore.md) · Conventional

## References

[^brooks1987]: Brooks, Frederick P. 1987. "[No Silver Bullet: Essence and Accidents of Software Engineering](https://www.cs.unc.edu/techreports/86-020.pdf)." *Computer* 20 (4): 10–19. <https://doi.org/10.1109/MC.1987.1663532>.
[^murphyhill2009]: Murphy-Hill, Emerson, Chris Parnin, and Andrew P. Black. 2009. "[How We Refactor, and How We Know It](https://sites.cc.gatech.edu/reverse/repository/refactoringpractice.pdf)." *Proceedings of the 31st International Conference on Software Engineering (ICSE '09)*, 287–97. <https://doi.org/10.1109/ICSE.2009.5070529>.
[^negara2014]: Negara, Stas, Mihai Codoban, Danny Dig, and Ralph E. Johnson. 2014. "[Mining Fine-Grained Code Changes to Detect Unknown Change Patterns](http://dig.cs.illinois.edu/papers/ICSE14_Stas.pdf)." *Proceedings of the 36th International Conference on Software Engineering (ICSE '14)*, 803–13. <https://doi.org/10.1145/2568225.2568317>.
[^opdyke1992]: Opdyke, William F. 1992. "[Refactoring Object-Oriented Frameworks](https://www.laputan.org/pub/papers/opdyke-thesis.pdf)." PhD thesis, University of Illinois at Urbana-Champaign. <https://www.laputan.org/pub/papers/opdyke-thesis.pdf>.
[^fowler1999]: Fowler, Martin. 1999. *[Refactoring: Improving the Design of Existing Code](https://archive.org/details/isbn_9780201485677)*. Addison-Wesley Professional. [https://archive.org/details/isbn\\\_9780201485677](https://archive.org/details/isbn\_9780201485677).

## Acronyms

- ADR — architecture decision record
