When (and Why) to Refactor

When (and Why) to Refactor

Small safe steps, for economic reasons, not cosmetic ones

4 min read

Refactoring is one of the most overloaded words in our field. People use it to mean "I rewrote a big chunk and it was broken for three days." That is not refactoring. Let me be precise about what the word means and when the practice is worth doing.

What refactoring actually is#

Refactoring is changing the internal structure of code without changing its observable behavior. The program does the same thing before and after; only its shape improves. That definition, which Martin Fowler lays out carefully in Refactoring, has a sharp edge to it.

The tell that someone was not really refactoring is that their code was broken for days. Real refactoring happens in tiny steps where the code is never broken for more than a minute or two. You make a small structural change, the tests still pass, you make another, and those small safe steps compose into large changes over time. If you are sitting on a broken build for an afternoon, you are doing some other, riskier thing — restructuring, maybe, but not refactoring in this disciplined sense.

Why we do it: economics, not aesthetics#

The case for refactoring is economic, not moral. We do not refactor to make the code pretty or to satisfy some sense of craft. We refactor because it makes us faster — faster to add the next feature, faster to fix the next bug. Clean structure is a means to velocity, and that is the justification you bring to anyone asking why you are spending time on it.

When to refactor#

The triggers are mostly moments of friction:

  • When adding a change or fixing a bug is not easy — the difficulty is the code telling you its structure is wrong for what you now need.
  • When you have to change code you do not understand. Refactor toward understanding; restructure it until it makes sense.
  • When you understand the code but can see it is done badly.
  • When the logic is needlessly convoluted and could be expressed more simply.
  • When nearly identical functions could be collapsed into one by parameterizing the difference.

Refactoring entry points#

I think of refactoring as having two modes. Most of it should be opportunistic — woven into normal work rather than scheduled as its own task:

  • Preparatory refactoring — refactor to make a feature easier to add, right before you add it. The clean-up and the feature land together.
  • Comprehension refactoring — once you have figured out what a confusing piece of code does, push that understanding back into the code so the next person does not have to re-derive it.
  • Litter-pickup refactoring — tidy small things as you pass through, the way you would pick up a stray bit of trash on a trail.

The other mode is planned — long-term, larger refactoring done incrementally over many small steps rather than in one big bang. This is the right approach for a structural problem too big to fix opportunistically.

When not to refactor#

Do not refactor code you do not have to touch. Ugly code that you can treat as a stable API — you call it, it works, you never need to read its insides — can stay ugly indefinitely. The day you actually need to understand or modify it is the day it earns refactoring, and not before. Refactoring code for its own sake, with no upcoming reason to touch it, is just risk without return.

What makes it safe#

Two foundations make refactoring safe enough to do constantly:

  • Self-testing code — a test suite you trust to catch a behavior change the instant you introduce one.
  • Continuous integration — frequent merges so your small steps do not pile up into a merge conflict that undoes the benefit.

Without those, every refactoring is a gamble. With them, it is routine.

On performance#

A common worry is that refactoring will hurt performance. The rule is: measure, do not speculate. Most of the time, finish the refactoring first and tune afterward, against real measurements, on the actual hot paths a profiler points you to. Guessing about performance while you restructure usually makes the code worse on both fronts.

A worked example: swapping a library#

Swapping one library for another is far easier if you first introduce an abstraction that can front either one. Put a thin interface in place, route the existing library behind it, get everything green, and only then build the second implementation behind the same seam. That technique is Branch by Abstraction, and it is refactoring in the truest sense: small steps, never broken for long, structure improving until the swap becomes almost boring.