Refactor, Rewrite, or Strangle
Overview
Section titled “Overview”Three available moves, and the choice between them is made badly more often than almost any other architectural decision — usually in favour of the one that feels most decisive.
The useful framing is not which is best practice. It is what each one requires in order to work, because the requirements are very different and only one of them is cheap.
Refactor
Section titled “Refactor”Change the internal structure without changing behaviour, in small steps, with the system running throughout.
What it requires: a way to know you have not changed behaviour. That is the entire condition. With tests covering the area, refactoring is routine; without them, every step is a gamble and the work stops being refactoring at all — it becomes rewriting in small increments with no safety net.
Where it wins: anywhere the area is well understood and the structure is the problem. It is also the only one of the three that delivers continuously by default, since the system never stops working.
The failure mode: refactoring an area that is past the point where incremental change can reach the problem. If the data model itself is wrong, no amount of local tidying fixes it, and a year of diligent refactoring can leave the fundamental shape untouched.
Rewrite
Section titled “Rewrite”Build the replacement separately, then switch.
What it requires: that the existing system’s behaviour is actually known. This is where rewrites die. A system that has been in production for years encodes thousands of decisions nobody remembers — edge cases, workarounds for customer-specific behaviour, quiet fixes for problems that recurred. The rewrite reproduces the parts somebody remembered.
Where it wins: genuinely small systems, and systems whose requirements have changed so completely that the old behaviour is not worth preserving. Both are rarer than they appear.
The failure mode is well documented and keeps happening. The rewrite takes two or three times its estimate; the old system cannot be frozen because the business keeps needing changes; so both are maintained at once with the team split; the rewrite falls further behind the moving target; and it is abandoned or shipped incomplete. The reason this keeps happening is that a rewrite is the most appealing option to engineers and the easiest to sell to leadership — it promises a clean end state and a date.
Strangle
Section titled “Strangle”Route slices of behaviour away from the old system to new ones, until the old system is empty rather than replaced. Fowler named the pattern; Sam Newman’s Monolith to Microservices is the practical treatment, and the place to go for the decomposition patterns this topic only names.
What it requires: a seam. Somewhere a request can be intercepted and sent to either implementation, which in practice means an API boundary, a router, a proxy, or a facade you introduce for the purpose. Introducing that seam is often the real first project.
Where it wins: large systems that cannot stop, which is most of them. It is the only approach that keeps delivering value while the structural change is in progress, and the only one where stopping halfway leaves you better off than when you started.
The failure mode is specific and important: the migration stalls partway, and you maintain two systems indefinitely. The previous section’s arithmetic is exactly this risk made visible — a strangler that has moved a third of the traffic in two years has a real possibility of never completing, and the organisation must be able to live in the middle state for years. Which is survivable; being surprised by it is not.
graph TD
A{"Is behaviour<br/>covered by tests?"} -->|"Yes"| B["<b>Refactor</b><br/>continuous, safe,<br/>cannot fix shape"]
A -->|"No, and the<br/>system is large"| C["<b>Strangle</b><br/>needs a seam<br/><i>plan to live in the middle</i>"]
A -->|"No, and the system<br/>is small or obsolete"| D["<b>Rewrite</b><br/>needs behaviour<br/>to be <b>known</b>"]
classDef q fill:#5B8FB9,stroke:#4A7A9E,stroke-width:2px,color:#ffffff
classDef safe fill:#7D9A4E,stroke:#5F7A38,stroke-width:2px,color:#ffffff
classDef mid fill:#E8A33D,stroke:#C4832A,stroke-width:2px,color:#ffffff
classDef risk fill:#D9695B,stroke:#B4483A,stroke-width:2px,color:#ffffff
class A q
class B safe
class C mid
class D risk
One question gets you most of the way. The third branch is the one to be suspicious of, because it is the one people want.
The two techniques that make the others possible
Section titled “The two techniques that make the others possible”Branch by abstraction. Introduce an abstraction over the thing you intend to replace, point all callers at it, then change what sits behind it. This is how you create a seam in a system that has none, and it is what converts an impossible refactor into a possible one. It costs an extra layer that is temporary in principle and permanent in practice often enough to be worth noting.
Characterization tests. Tests that capture what the system currently does, including behaviour nobody intended, so that change can be made safely. Michael Feathers’ technique, and the correct first move in any area with no coverage. The crucial discipline: a characterization test asserts actual behaviour, bugs included — it is not a specification, and writing it as one defeats the purpose. The next topic but one has a sharp warning about this.
Parallel run completes the set: run both implementations on real traffic, compare outputs, and only switch when they agree. Expensive, and the right call when being wrong is costly — which for an action-taking system it is.
Feeds the workbook
Section titled “Feeds the workbook”This decides the shape of your legacy strategy — specifically, whether the AI platform competes with the migration for capacity or produces seams the migration needs. That distinction is the strongest argument available to you.
→ The strategy is clear. The capacity to execute it does not exist. — Sequencing under zero spare capacity