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 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?