Rewrite, Refactor, or Strangle? A Practical Decision Framework

In Brief
Refactoring improves internal structure through behaviour‑preserving changes; rewriting replaces an implementation and may preserve or change its behaviour. A strangler approach is an incremental migration strategy, replacing the old system gradually rather than at one cutover. It can include both rewriting and refactoring.
A frustrating codebase can make a rewrite sound very attractive. Delivery has slowed, incident recovery is awkward, and simple changes take far more work than they should. Replacing the code might help, but it also leaves you with a migration to deliver.
Refactoring Changes Structure; Rewriting Replaces an Implementation
Refactoring has a narrower meaning than "doing some work on old code". In Martin Fowler's definition, it improves internal structure without changing observable behaviour. A sequence of small, behaviour‑preserving transformations can produce a substantial architectural improvement; the size of the eventual result doesn't make it a rewrite.
Extracting duplicated price‑calculation logic into a shared function can be refactoring if callers still receive the same results. Changing the rounding rule at the same time is a separate behaviour change. Both may be worthwhile. I'd keep them separate so the reviewer can tell whether a changed result is intentional.
A rewrite replaces an implementation. It may deliberately preserve the same public behaviour, introduce new behaviour, or do both. Keeping the same features doesn't make a replacement a refactor. You still need to decide how much to replace and how to check that it behaves as intended.
Nor does calling a task a refactor make it safe. The behaviour to preserve includes relevant side effects, errors, data contracts, and operational constraints, not just the most obvious return value. Characterisation tests and smaller changes can help establish that boundary where documentation is incomplete.
Fowler's original 2004 strangler fig article describes a way to replace a system gradually around its edges. A component can be rewritten, moved, or refactored within that migration. The migration boundary, routing, and retirement of old behaviour need planning alongside the code changes.
What Can Survive the Change
I'd start with what still works. A useful domain model, reliable releases, and a team that understands the system are all reasons to consider incremental improvement or replacement. They're worth keeping if they can support the work ahead. If they can't, preserving them may simply prolong the problem.
For a rewrite, I'd want to see how we'll move users and data onto the replacement. For refactoring, I'd want evidence that the behaviour stays the same. For a strangler migration, I'd look for somewhere to separate the systems and check we can afford to run both until the old path can be retired.
The Cost of Starting Again
Being tired of the old code isn't enough reason to replace it. The business may not be able to absorb a delivery freeze or the risk of moving everything at once, however appealing the new design looks.
The opposite mistake is to keep making small changes when the same underlying problem obstructs every release. That can cost just as much time without getting you any closer to a workable system.
Before Committing
I'd look at change cost, operational risk, delivery continuity, and organisational memory. That means measuring lead time and incident recovery, finding out where knowledge is concentrated, and testing whether part of the system can be extracted. For a gradual replacement, check that the organisation can fund the period when old and new run together.
Selfridges
When I joined Selfridges in 2020, three partial modernisation attempts had left three codebases, with many changes repeated across all three. I started with a small experiment: could a React component from the newest implementation run in each section? Once that worked, we could introduce shared components progressively whilst continuing to deliver customer‑facing features.
Looking back through these four factors, repeated implementation was the change cost. The small integration experiment tested the operational risk, the existing sections kept delivery moving, and their behaviour remained available to guide the work. Refactoring each codebase separately would have left the duplication in place. A fourth replacement could have become another unfinished platform. That made progressive replacement a useful fit for this situation, rather than a reason to choose it for every project.
Wrapping Up
At each migration milestone, I'd check whether we can still separate the systems where we expected, what running both is costing, and whether delivery is slowing down. I'd also want to see old code being retired. Otherwise, the supposed migration may just be adding another system to maintain.
Key Takeaways
- Refactoring preserves observable behaviour. A rewrite replaces an implementation, even if it keeps the same features.
- A strangler migration replaces a system gradually and can contain both refactoring and rewriting.
- Test a small integration before committing to the migration. At Selfridges, that experiment established whether shared components could work across the existing implementations.
- Budget for running old and new together, and make retiring the old paths part of the work.