When to Rebuild vs Refactor Legacy Software

When to rebuild vs refactor legacy software, decided by evidence instead of disgust: the seam test, the strangler path, and the four rebuilds worth doing.
Nobody Rewrites a System They Can Still Change
The question of when to rebuild vs refactor legacy software almost never arrives as a question. It arrives as a rewrite proposal, in a very particular emotional register. Someone new joined, read the code, and came back visibly upset. The stack is old, the tests are theatre, and there is a 4,000-line file everyone calls "the God object" without irony. The pitch writes itself: six months, clean architecture, modern framework, and we never speak of this again.
Deciding when to rebuild vs refactor legacy software on that basis is how companies lose two years. Disgust is a real signal — engineers are usually right that the code is bad — but it says nothing about which intervention is cheaper, and it systematically underprices the one thing the legacy system already has: a decade of correct behaviour that nobody wrote down.
At Kuaray we get called in on both sides of this, sometimes to finish a stalled rewrite and sometimes to talk a team out of starting one. The decision is more tractable than the arguing suggests, but only if you stop grading the codebase and start grading your ability to change it.
When to Rebuild vs Refactor Legacy Software: Grade the Change, Not the Code
Ugly code that ships weekly is not a legacy problem. It's just ugly. The actual condition worth treating is when the cost of a change stops tracking the size of the change — when a one-line business rule takes three weeks because nobody can predict what it touches, and the release that follows breaks something unrelated.
So run the diagnosis on the change, not the source. Three measurements, all of which you can get from tooling you already have:
Lead time for a trivial change. Pick the last five one-field changes and measure commit-to-production. If a text label takes a week, the constraint is process or coupling, not language choice, and a rebuild inherits both.
Change failure rate by module. Pull the last year of incidents and map them to directories. Legacy pain is almost never uniform. In most systems we assess, a large majority of production incidents cluster in a minority of the codebase — and that minority is what you actually need to fix.
Blast radius per commit. Use git history to count which files habitually change together. Files with high co-change coupling are your real architecture, whatever the diagram says. If everything co-changes with everything, you have a distributed monolith and no seams — that's the condition where refactoring genuinely stalls.
Those three numbers turn a taste argument into an engineering one. They also tend to localise the problem: the answer is frequently "rebuild these two modules, leave the other forty alone," which is neither of the options that were on the table.
What a Rewrite Actually Costs
The build estimate is not the cost. Three line items get left out of every legacy system modernization pitch, and together they usually exceed the development number.
Dual maintenance. While you build v2, v1 keeps taking bug fixes and regulatory changes. Every one of those has to be ported. You are now running two systems with one team, and the old one has all the users.
Feature parity is a moving target. The old system does things nobody remembers implementing — a Tuesday-only export, a rounding rule for one enterprise account, an undocumented API a partner still calls. You will discover these in production, from customers.
Knowledge loss. The behaviour of a fifteen-year-old system is documented in the system. Not in the wiki. A rebuild throws away the specification and the implementation simultaneously, which is why big bang rewrite risk is structurally different from ordinary project risk: there's no partial credit. A half-finished refactor still ships. A half-finished rewrite is a liability with a burn rate.
Refactoring has an honest cost too, and it's rarely admitted: it takes discipline the organisation may not have. A refactor programme with no protected capacity dies in the third sprint when a customer escalation lands, and you're left with a codebase that's half-migrated in a way that's worse than either endpoint.
The Rubric
Score these row by row. Rows are not equal — seams and staffing dominate everything below them.
| Signal | Points to refactor | Points to rebuild |
|---|---|---|
| Seams | Modules have boundaries you can test against | Everything reaches into everything; no seam to cut |
| Test coverage | Behaviour is pinned, or can be pinned characterisation-style | No tests and the behaviour can't be observed from outside |
| Staffing | You can hire for the stack today | Runtime is EOL, no security patches, nobody will take the job |
| Failure clustering | Incidents concentrate in a few modules | Failures are uniform across the system |
| Data model | Schema is sane; logic is the mess | The schema itself encodes the wrong domain |
| Requirements | The system does roughly the right thing | The business it was built for no longer exists |
| Compliance | Controls can be retrofitted at the boundary | Audit or residency rules are unsatisfiable in this architecture |
| Traffic profile | Current load is survivable | Architecture caps you an order of magnitude below where you're going |
| Ownership | Someone understands it | Original team gone, no documentation, no reader |
The pattern in the right-hand column is worth naming. Legitimate rebuild cases are almost all cases where the model is wrong, not where the code is bad. Bad code is a refactoring problem — that's literally the definition. A wrong domain model, a schema that made a false assumption in 2014, an architecture that can't express what the business now sells: those don't refactor away, because every incremental step has to keep satisfying the wrong model.
Four Rebuilds Worth Doing
Being specific matters more than being balanced here. These are the situations where we've seen a full rebuild pay for itself:
- The runtime is end-of-life and unpatched. A framework with no security releases isn't technical debt, it's an open finding. Refactoring inside it doesn't move the date. This one has a deadline set by someone else.
- The data model contradicts the business. If "customer" is one row and the business now sells to organisations with hierarchies, every feature is a workaround. Rebuild the model, migrate the data, keep the UI if you can.
- The system can't be observed. No logs, no traces, no way to assert correctness from outside the process. You can't safely refactor what you can't measure — and adding the instrumentation is sometimes a larger job than the replacement.
- The scope is genuinely small. A 6,000-line service with a clear contract and two consumers is not a rewrite, it's a Tuesday. Size, not sentiment, is what makes big bang risky.
Notice what isn't on the list: the language is unfashionable, the previous team made choices you disagree with, the new hire wants to use a different framework. Those are real morale costs and they're worth taking seriously as morale costs. They are not architectural arguments.
The Path That Usually Wins: Strangle It
For anything above roughly one team-quarter, the answer to when to rebuild vs refactor legacy software is usually "both, incrementally." The strangler fig pattern is well-known and badly executed, so here's the order that works:
- Put a boundary in front. A façade, gateway, or router that all traffic passes through. Nothing has changed yet — you've just bought a place to stand.
- Write characterisation tests, not correctness tests. Record what the system currently does, including the parts you think are bugs. Your migration target is identical behaviour, and you renegotiate the bugs later, in daylight.
- Cut the first slice along a data boundary, not a UI boundary. Pick a bounded context that owns its tables. Slicing by screen produces two systems fighting over one database, which is the failure mode that gives incremental migration a bad name.
- Run both and compare. Shadow traffic to the new implementation, discard its output, diff against the old one. Discrepancies are free findings — this is where the Tuesday-only export surfaces, before customers find it.
- Flip with a switch you can flip back. Route a percentage. Keep the old path warm until the new one has survived a full business cycle, including month-end and whatever your seasonal peak is.
- Delete the old path on a date. Written down, with an owner. Skip this and you've added a system instead of replacing one, permanently.
- Repeat, and stop when the pain stops. The goal is a system you can change, not a completed migration. Plenty of strangler programmes should end with 60% migrated and everyone content.
The reason this beats both pure options is the same reason reversibility beats optimality in a build vs buy software decision framework: you get to keep deciding as you learn what the requirements actually were. Every slice ships value, and any slice is a legitimate stopping point.
It's slower on paper. It's faster in practice, because the two costs that kill rewrites — dual maintenance and parity discovery — get amortised into small pieces instead of arriving all at once, eighteen months in, when the deadline has already been re-committed twice.
Before You Decide Anything
Two weeks of evidence beats two months of arguing. Instrument the current system, pull the incident-to-module map, run the co-change analysis, and write down the five changes the business will ask for next year. If the legacy system can absorb those five changes at acceptable cost, the code being ugly is a quality-of-life issue, and there are cheaper fixes for that than a rewrite.
If it can't — if the model is wrong, the runtime is unsupported, or there is no seam anywhere to cut — then you're not choosing to rebuild. You already have to, and the only remaining question is whether you do it in slices or in one heroic leap. Choose slices.
Talk to Kuaray about your legacy system — we run the assessment first, tell you honestly which modules deserve a rewrite and which just need tests, and we'll happily argue you out of the rewrite if the evidence says so. See how we approach custom software and DevOps and delivery, or read our MVP approach if the replacement starts as a thin slice you need in production fast.