Skip to content

What Is Refactoring?

People use refactoring loosely to mean “cleaning up code.” The discipline means something sharper:

A refactoring is a change to the code that improves its internal structure while preserving its observable behavior.

Two halves matter equally. The change must improve structure — otherwise it is pointless churn. And it must preserve behavior — every input still produces the same output, every side effect still happens, no public contract shifts. A change that alters what callers can observe is not a refactoring, no matter how tidy the result looks.

Refactoring vs. rewriting vs. adding features

Section titled “Refactoring vs. rewriting vs. adding features”

These three activities feel similar from a chair, but they carry very different risks.

  • Refactoring reshapes existing, working code in small reversible steps. You always have a running program; you are never far from the last green state.
  • Rewriting throws the old code away and builds a replacement. You lose the accumulated bug fixes baked into the original, and you have no running program until the new one is done. Sometimes necessary, rarely cheap.
  • Adding a feature introduces new behavior. The program does something it could not do before, which means its observable behavior changes by design.

Refactoring is the safe middle path: you keep the working code and improve it gradually, rather than gambling on a from-scratch replacement.

When you sit down to edit code, you are wearing one of two hats. The feature hat is for adding capability: you write new code and the tests for it. The refactor hat is for restructuring: you change how the code is shaped, and the existing tests must stay green the whole time because behavior must not move.

The metaphor is Kent Beck’s, as Martin Fowler records in Refactoring (2nd ed., 2018, ch. 2, “The Two Hats”). The rule is simple and strict: wear only one hat at a time. If you are halfway through extracting a function and notice a missing feature, finish the refactor, commit, then switch hats. Mixing the two is how a “quick cleanup” turns into an afternoon of debugging — when a test breaks, you can no longer tell whether your restructuring or your new feature caused it.

flowchart TB
  Start["I want to work on the code"] --> Q{"Which hat?"}
  Q -->|"Adding behavior"| Feature["Feature hat:<br/>write new code,<br/>add tests"]
  Q -->|"Cleaning structure"| Refactor["Refactor hat:<br/>keep behavior,<br/>tests stay green"]
  Feature --> Switch["Switch hats —<br/>never wear both"]
  Refactor --> Switch
The two hats: keep restructuring and feature work strictly separate

The smallest refactorings are often the most valuable. Here we do two: replace a magic number with a named constant, and rename a cryptic variable. Behavior is identical before and after — the output is the same — but the intent now reads off the page.

// Before
function price(q: number): number {
return q * 9.99 * 1.07;
}
// After
const UNIT_PRICE = 9.99;
const TAX_RATE = 1.07;
function price(quantity: number): number {
return quantity * UNIT_PRICE * TAX_RATE;
}

Nothing the function returns has changed. That is the whole point: a refactoring you can run before and after, and get byte-for-byte identical results, is a refactoring you can trust.

Which change qualifies as a refactoring?
What distinguishes refactoring from rewriting?
What does the "two hats" rule tell you to do?
In the before/after example, how do you know it was a true refactoring?