Refactoring is changing code structure without changing behavior. The classic Fowler definition. In practice, real-world refactoring lives between two failure modes: the "big bang" rewrite that is supposed to take six weeks and takes six months, and the "leave it alone" approach that lets entropy compound.
The strategies below are the structured alternatives — how to evolve code incrementally, with each step verifiable, while still ending up at the destination.
Named after the strangler fig tree that grows around a host tree until eventually the host is gone. In code: build the new system next to the old one, gradually route traffic from old to new, eventually retire the old.
Steps:
Strangler is the safest pattern for high-stakes systems. It is also slow — running both systems in parallel costs effort. The trade-off: maximum safety, maximum cost.
For changes to interfaces or data structures that have many callers:
Each step is independently safe. The change can take days or months without breaking anything in between.
When the change is structural (replacing one component with another):
Similar to strangler but at smaller scale, internal to a service rather than across services.
When a change is hard, the first refactor is the one that makes it easy. Often this means:
Then make the actual change. The two-step approach often produces a cleaner result than trying to make the structural change and the behavior change at once.
Big bang refactors fail more often than they succeed. The reasons:
Incremental refactoring solves these but takes longer and requires sustained discipline. Most successful "rewrites" are actually long sequences of incremental refactors.
Pure refactoring preserves behavior — including bugs. Refactoring that fixes bugs at the same time blurs the line between refactor and feature change.
The discipline: do them separately. Refactor, verify behavior is identical, then fix the bug as a separate change. Mixing them makes either step harder to review.
Refactoring is dramatically safer with good test coverage of the affected behavior. Without tests, refactoring is guess-and-pray.
The chicken-and-egg: legacy code without tests cannot be refactored safely, but cannot be tested without first refactoring. The escape: characterization tests (snapshot the current behavior, treat that as a temporary spec) that lock in observable behavior, then refactor under that net.
The most common, lowest-risk refactor. Take a chunk of code that does one thing and pull it into a named function. Often makes the calling code more readable.
Tools support this directly in modern IDEs (with name verification, callsite updates). Use the tool, not manual extraction.
Renaming a function, variable, or type. Trivial-looking but high-leverage if done at scale. A bad name appearing 200 times is 200 readers stumbling.
A method belongs on a different class than where it lives. Move it. The original class often has weaker dependencies after the move.
The opposite of extract. A function that adds nothing — just calls another function with no transformation — is noise. Inline it.
A "currency amount" represented as a number is fragile. Currency conversions get done by hand; mistakes hide. Replacing the number with a Money object that carries currency and amount eliminates the class of bug.
A switch statement that varies behavior by type can sometimes be cleaner as polymorphism. Sometimes — the switch is often clearer for small cases. Use polymorphism when the cases are stable and the behavior varies substantially.