Preparatory Refactoring
also called Enabling Refactor, Prepare-Then-Change
Reshaping existing code first so the behaviour change that follows is small, keeping the two in separate commits with separate claims about what is true.
You need to add a second payment provider. The current provider's quirks are threaded through a 600-line method: a retry here, a rounding rule there. The fast route is a flag and a branch, a 40-line diff inside a method that is now 700 lines long with two providers' assumptions interleaved.
The other route spends a day first: extract the provider's behaviour behind an interface, move the existing logic into one implementation, change nothing observable. Then the new provider is a new file and a 50-line diff.
Both routes ship the feature this week. They differ in what a reviewer can check and what you can revert on the day it breaks.
Why it matters
A commit mixing restructuring with new behaviour makes two claims and verifies neither. The refactoring commit's claim is "the tests did not change and they are green". The feature commit's claim is "here is new behaviour and the test that proves it". Mixed, the first is unavailable because tests changed, and the second is buried.
The consequences arrive later. A bisect lands on the mixed commit and says nothing about which half is at fault, and the revert is unavailable, because reverting the bug also reverts restructuring other work now sits on. Under pressure the team forward-fixes instead, which is how one bug becomes three.
The clean-up-afterwards commit also has a poor record of arriving: once the feature ships, the business reason for touching the code has gone.
Implementation patterns
- Kent Beck's rule, which Martin Fowler writes up as preparatory refactoring: make the change easy, then make the easy change. The feature justifies the preparation, which is also what stops the preparation sprawling.
- Do not touch tests in the refactoring commit. If a test had to change, it was not a refactoring and the message is wrong.
- Keep preparation inside the feature's blast radius, and review it as two commits in one pull request, or two pull requests when it exceeds about a day. Preparation reaching modules the feature never touches is separate work needing its own justification.
- When the right shape is not knowable yet, invert the order deliberately. Write the feature in place, learn what the code wants to be, and make that refactoring the first commit of the next change in the area.
Industry example
The practice is documented rather than proprietary. Fowler's Refactoring (first edition 1999, second 2018) separates the two hats: refactoring adds no function, adding function does not restructure, and you switch often but never wear both.
The archetype worth picturing is the review. A 640-line mixed diff gets "looks good" in ten minutes, because the reviewer cannot tell moved code from new code. Split in two, the same work draws a real objection on the 50-line half, where the bug lives.
Failure scenarios
- The refactoring that was not one. A field becomes eagerly evaluated, an iteration order changes, an exception type changes. Review passes and a nightly job fails next week.
- Preparation without a stop. The extraction uncovers another and the feature slips a sprint, which is the pattern that makes managers distrust the practice.
- The long-lived preparation branch. Restructuring conflicts with every open branch in the package, and conflict probability climbs with branch age.
Trade-offs
| Choose | Gains | Pays |
|---|---|---|
| Prepare first | A small reviewable behaviour diff and a working revert; the next change in that area is cheaper | A day before the feature starts, plus a conflict window for anyone in the same files |
| Change in place | Ships today with no coordination | The next change pays more, and the one after that pays more again |
When not to use it
An experiment behind a flag with a deletion date earns no preparation. Write it crudely, keep it isolated so deletion is one revert, and carry the shape lesson into the real implementation if it survives.
The same holds at 2 a.m. during an incident, where the only correct instinct is the smallest change that restores service, and for a design you do not yet understand. Two payment providers is a guess about where variation lives; the third is evidence. Preparing for the second can produce an abstraction with one shape in it, harder to change than the duplication it replaced.
Interview question
Q: A pull request adds a feature in 640 lines, 600 of which are moved code. Walk me through what you ask the author to do and the argument you make for it, then tell me when you would approve it as it stands.
What a strong answer covers: splitting into a behaviour-preserving commit and a behaviour-changing one, and what each claims; the operational reason rather than the aesthetic one, bisect and revert granularity; the rule that a refactoring commit does not change tests; and the exception of a time-critical fix or throwaway code.
Quick check
Quiz: Why keep restructuring and new behaviour in separate commits rather than separate sections of one diff? — Each commit then makes one checkable claim, and bisect and revert operate on halves you can act on.
Flashcard: What is the test for whether a commit was a refactoring? — The tests did not change and they are green. If a test had to change, behaviour changed and the commit is mislabelled.