When to rebuild vs refactor legacy software, decided
Almost nobody asking this question has two options. They have three, and the one they have not considered is the one the people who do this for a living reach for first.
The conversation starts the same way every time. Something is slow or fragile, a developer says the code is a mess, and somebody asks whether it is time to start again. Within a week the question has hardened into a binary: rebuild, or keep patching?
That framing is the problem. Rebuild and refactor are not two ends of a spectrum you pick a point on. They are specific strategies with different failure modes, and there is a third that fits more situations than either, which most teams have never been offered because it has no obvious name in a budget meeting.
Why the from-scratch rewrite keeps failing
Start with the option everyone's instinct reaches for, because the argument against it is older and better evidenced than most people realise. Martin Fowler's Strangler Fig Application describes the obvious plan, building a new system that does exactly the same thing in better technology, and says plainly that such projects have usually gone down in flames.
He gives three reasons, and they are worth separating because they fail at different times. The first is duration: replacing a serious system takes a long time, and the users cannot wait for new features while it happens. The second is specification: the replacement looks easy to specify, but the existing behaviour turns out to be hard to pin down. The third is the one that stings, because much of the existing behaviour is unwanted, so faithfully reproducing it wastes effort.
Notice that none of these is about the quality of the new code or the skill of the team. They are structural. A better team building a better system hits all three on the same schedule, which is why the rewrite that fails is so often the one run by the people who were most confident.
The second reason deserves a moment on its own. The real specification of the old system is its behaviour, including the parts nobody designed: the report a department depends on, the field that means something different after 2019, the overnight job whose timing three other things quietly rely on. A rewrite promises to reproduce that and then finds it is reverse-engineering it, one support ticket at a time, after go-live.
The question that actually decides it
Here is the thing the debate usually gets wrong. Age does not decide this. Nor does ugliness, nor the language it was written in, nor how much the current developers dislike it. Those are the arguments people bring, and none of them predicts anything about cost.
Three questions do most of the work:
- Is the behaviour knowable? Can somebody, today, tell you what the system is supposed to do in enough detail to check a replacement against it? If the answer is a person rather than a document or a test suite, your rewrite risk is much higher than your estimate assumes.
- Are there seams? Can you draw a line around a piece of this system and replace that piece alone? Fowler notes that legacy systems seldom have clean independently replaceable components, so teams have to find or insert seams. Whether you can is the difference between an incremental path and an all-or-nothing one.
- Is it still changing? A system nobody needs to modify is not a problem, whatever it looks like inside. Ugly and stable is a solved problem. The cost of bad code is paid only when you change it.
A fourth, less comfortable one: is the thing you want actually a software change? A surprising share of rebuild requests are really requests for a process to be different, and those survive a rewrite intact. We have written about the general shape of that in why software projects fail, and it is the cheapest check available because it costs one conversation.
Situation by situation
| What you actually have | The right move | Why |
|---|---|---|
| Ugly code, stable requirements, nobody changing it | Leave it | The cost of bad structure is only paid on change. No change, no cost |
| Slow in one known place, rest is fine | Refactor that place | A targeted fix with a measurable before and after beats a programme |
| Changes keep breaking unrelated things | Refactor behind tests, seams first | This is a coupling problem, and a rewrite reproduces it unless you know why it happened |
| Whole areas nobody dares touch, but the system works | Incremental replacement | You need to move pieces out without a big-bang cutover |
| Platform is end of life or unhostable | Rebuild, but by piece where possible | The deadline is external, which changes the maths |
| The business model it encodes no longer exists | Rebuild | You are not replacing software, you are replacing the thing it models |
Two rows in that table are the common misdiagnosis. Teams read "changes keep breaking unrelated things" as evidence the code is beyond saving, when it is the most treatable condition on the list. And they read "ugly" as urgent, when it is the only row where doing nothing is correct.
The third option, and how it actually works
The strategy most teams are never offered is incremental replacement: you do build new code, but you move the system across one piece at a time while it stays in production throughout. Fowler's name for it is the strangler fig, after the figs that germinate in a nook of a host tree and gradually grow around it until they can stand on their own.
Microsoft's Azure Architecture Center entry for the pattern is the more operational of the two write-ups, and it names the mechanism: a facade, effectively a proxy, sits between the client and the back end and intercepts requests. Initially it routes most of them to the legacy system. As functionality moves, it shifts more to the new one. When nothing depends on the legacy system any more, you decommission it, and finally you remove the facade and let clients talk to the new system directly.
The reason this is worth the extra machinery is that it changes what a failure costs. Each step is small and reversible, the system is in production the whole time, and the business keeps getting features. You also learn the old system's real behaviour by moving it, rather than by trying to specify it in advance, which is the rewrite's second failure mode neutralised.
It is not free, and the Microsoft page is honest about the costs. The facade must keep up with the migration, and must not become a single point of failure or a performance bottleneck. Both systems coexist and call each other, which the page recommends handling with an anti-corruption layer so the new system does not quietly adopt legacy conventions. Shared data stores need both systems able to reach them at once, usually the hardest part of the exercise. The page frames the facade as transitional architecture, weighed as risk mitigation against temporary infrastructure cost.
If pieces of the old system will keep running alongside the new indefinitely, the integration between them stops being temporary scaffolding and becomes a thing you own. The costs of that are in API integration between two systems, and they are routinely left out of rebuild estimates.
Where the incremental path does not apply
This matters, because the pattern gets recommended reflexively. The Microsoft page lists the cases where it is unsuitable, and they are concrete: when requests to the back end cannot be intercepted; when you cannot access the legacy source, since disabling migrated features and redirecting internal calls requires modifying it; when the system is small enough that replacing it wholesale is simple; and when you need the original fully decommissioned quickly.
Fowler adds the caveat most likely to catch an organisation out. Legacy systems reflect the design thinking and processes that produced them, so without changing how the team is organised you get a new system resembling the old one. He points at Conway's Law for the reason: if the same four teams with the same handoffs rebuild it, the seams in version two fall in the same places.
What a genuine rebuild looks like
Sometimes it really is the answer, and the case is narrower than enthusiasm suggests. The strongest is when the business model the software encodes no longer exists. A system built around one-off sales, asked to handle subscriptions and entitlements, is not badly written. It is correctly written for a company that no longer operates, and no refactor reaches that, because the data model is wrong at the root.
The second is a genuine external deadline: a platform that is end of life, a runtime nobody will host, a compliance requirement the architecture cannot satisfy. Here the deadline is not negotiable, so the comparison is not rebuild against refactor but rebuild against the cost of missing the date.
If you are in one of those, two things make it survivable. Keep the old system running until the new one has been in production long enough to surface the behaviour nobody documented, which is longer than the plan allows for. And do it by piece where the seams permit, even inside a rebuild, because a sequence of small cutovers fails more cheaply than one large one.
Worth separating this decision from the build-or-buy one, incidentally. Once you have concluded the current system has to go, whether its replacement should be custom at all is a fresh question with a different answer, and we have gone through it in custom software vs off the shelf.
The honest limit
Nothing above tells you how long any of this takes, and we are not going to give you a multiplier. Estimates for legacy work are unreliable for the same reason the work is hard: the unknown behaviour is unknown, so it is not in the estimate. Anyone quoting a confident number for a rewrite of a system they have not instrumented is quoting the part they can see.
What you can do is make the first decision small. Pick one seam, move one piece, and measure what it actually cost. That number is worth more than any estimate, including ours, and it is available in weeks rather than quarters.
And a limit on the framing itself: "legacy" is doing a lot of unexamined work in most of these conversations. It usually means code written by people who are no longer here, which is a staffing fact rather than a technical one. Before committing anyone's budget, it is worth checking whether the problem is the system or the fact that only one person understands it, because those have very different and very differently priced solutions. The related confusion about what technical debt is and is not is in what is technical debt.
Describe it. We build it.
Seven or twelve days, pay on delivery, a year of maintenance included. Bring the problem, not a spec.
Book a meetingRead next
A purchase request approval workflow, and the thresholds
A purchase request approval workflow: who approves at which amount, what the platforms you already pay for will do, and when to configure instead of build.
ReadAI email triage for shared inboxes, and the human check
AI email triage for shared inboxes, scoped from the mailbox APIs up: what routing can do, what the notification leaves out, and where a person signs off.
Read