When to Refactor
Choosing the right moment, and recognizing the wrong one
When to Refactor
Refactoring has cost. The discipline is to refactor when the return justifies the cost — not on a schedule, not on aesthetics, and not in the middle of an unrelated change.
When to Refactor
Before adding a feature
Make the change easy, then make the easy change. — Kent Beck
When adding a feature is awkward, the awkwardness is information about the current shape of the code. Refactor first — into a shape where the new feature drops in naturally — then add the feature.
This is the highest-return moment to refactor: the work is justified by a feature that someone is paying for, the new shape is validated immediately by the feature, and the refactoring is bounded by the feature's scope.
To understand code
When reading unfamiliar code, capture the understanding as you go: rename the variable whose purpose you just figured out, extract the function whose intent you just identified. The next reader — possibly you, in a month — inherits the understanding instead of having to reconstruct it.
This converts one-time reading effort into permanent improvements to the codebase.
When fixing a bug
A bug that was hard to find is usually a bug that the structure helped to hide. After fixing it, refactor the surrounding code so the same class of bug becomes harder to introduce or easier to spot. The investment is repaid the next time something in the area changes.
The Rule of Three
The first time you do something, you just do it. The second time, you wince at the duplication, but you do it anyway. The third time, you refactor.
Premature abstraction is more expensive than duplication. Two similar pieces of code may diverge along axes you cannot yet predict; by the third instance, the shape of the abstraction is usually clear.
During code review
A small, mechanical refactoring inline — a rename, an extract — is often more useful in a review than a comment requesting one. The change is concrete, the reviewer can see the result, and the original author learns the pattern.
When Not to Refactor
When you cannot test the result
Refactoring without tests is a guess about behavior preservation. If the area is genuinely untested:
- Add characterization tests — tests that pin down the current behavior, whatever it is.
- Then refactor.
Skipping step 1 produces "refactorings" that quietly change behavior. The fact that the system still compiles is not evidence that it still works.
When a rewrite is cheaper
When the existing code is so unclear that even reading it for refactoring is harder than starting over, a rewrite may be the better choice. The decision hinges on:
- Is there a stable, well-understood specification of what the code should do?
- Is the cost of re-implementation lower than the cost of incremental improvement?
- Are there callers depending on undocumented behavior that a rewrite would break?
When the first two are yes and the third is no, rewrite. Otherwise, refactor.
Close to a deadline
Refactoring's payoff is medium-term; its cost — slower current change, risk of regression — is immediate. A deadline week is the wrong time to spend that cost.
The disciplined response is to ship the smallest viable change, log the technical debt explicitly, and refactor in the next iteration. Doing the refactor "while you are in there" is a common cause of missed deadlines and avoidable production incidents.
Code that nobody reads or changes
Code with no upcoming changes does not pay back the cost of refactoring. The exception is when the refactoring itself reveals defects worth fixing — but in that case, the goal is the bug fix, not the cleanup.
To satisfy a personal aesthetic
Style preferences that are not shared by the team should be discussed, agreed, and enforced through linters or conventions — not litigated through one-off refactorings. A refactoring that is contested in review and reverted next week was not worth doing.
The Cost Side of the Ledger
It is easy to talk about the benefits of refactoring; the costs deserve equal attention.
- Time. The hours spent refactoring are hours not spent shipping.
- Risk. Even with tests, refactoring can introduce regressions. The thinner the test coverage, the higher the risk.
- Cognitive load. Every team member must learn the new structure. Frequent restructuring without commensurate clarity is exhausting.
- Merge cost. Long-running parallel work conflicts badly with structural changes. Refactor on the trunk, not on a long-lived branch.
- Review cost. A large structural diff is difficult to review carefully; reviewers tend to approve out of fatigue.
Mitigations exist for each — small steps, mainline development, paired reviewer — but the costs do not vanish.
Working Practices
The mechanics of doing refactoring well:
Small commits
Each refactoring step is a candidate commit. Reviewing or reverting a hundred-step refactoring is feasible only if the steps are individually obvious.
Separate refactoring commits from feature commits
A pull request that mixes a refactoring with a behavior change forces the reviewer to verify both at once. Split: one PR refactors, the next PR uses the new structure for the feature. The reviewer's confidence in each is much higher.
Stay on the mainline
Long-lived refactoring branches are an anti-pattern. The merge cost grows superlinearly with the size of the change and the duration of the branch. Use mainline development with feature flags or branch-by-abstraction for changes too large to land in one go.
Communicate before invasive moves
A refactoring that touches many files, or files several teams own, deserves a heads-up. The cost is a short message; the saving is avoided merge conflicts and surprised teammates.
A Simple Decision Test
Before starting a refactoring, ask:
- Is there a concrete change coming that this makes easier?
- Are there tests that would catch a regression?
- Can I land this in small, reviewable steps?
- Is the cost — to me, to the team, to the deadline — justified by the return?
When all four answers are yes, refactor. When any one is no, fix that first or defer the work.