When Should You Rewrite Instead of Refactor?
Most rewrites are proposed for reasons a refactor would fix. The few that are justified share one property: the constraint sits below the code, where tidying cannot reach it.
On This Page

An engineer opens the meeting with a slide that says six months. The plan is to rebuild the billing service from scratch, in a newer framework, with the same behaviour but done properly this time. The room nods, because everybody has felt the friction the slide is describing. Nobody asks the one question that decides the whole thing: what, exactly, cannot be fixed inside the code that already runs?
Rewrite vs refactor gets settled by taste, seniority or fatigue far more often than by evidence. Our position is blunt: most rewrite proposals are refactor proposals with a larger budget attached. The honest test is not how the code reads. It is whether the thing blocking you lives in the code, or underneath it, where editing files cannot reach.
A small number of rewrites are correct, and they look nothing like the usual proposal. They are narrow. They name a constraint in one sentence. They almost always come with a plan to run two systems side by side for a while, and a date on which the old one gets deleted. This piece is about telling that kind apart from the other kind, and about the risk that appears on neither side of the usual comparison.
What a rewrite actually buys
A rewrite buys one thing reliably: a change of substrate. A different language, a different runtime, a different data store, a different deployment model. That is a real purchase and sometimes worth the money. Everything else people expect from a rewrite (better structure, clearer naming, fewer defects, faster delivery) is a property of how a team works rather than of when a file was created.
This is uncomfortable because it is testable. If the team that produced the tangle is the team writing the replacement, under the same deadlines, with the same review habits and the same tolerance for cleaning it up later, the replacement will be tangled by its second year. It will simply be tangled in a stack nobody has learned to work around yet.
The quality argument is a habits argument
Open the pull request that introduced the worst module in your system. Read the dates. It is rarely incompetence. It is usually a deadline with a shape, taken by somebody who knew what the better version looked like and did not have the week to build it. Bad code is a record of decisions made under pressure, and a new repository does not remove the pressure.
So if you want the next system to be better than this one, change something about how work is accepted before you change what it is written in. Review that is allowed to refuse. A test suite that runs in a time nobody resents. Enough operational visibility that a regression is noticed by a graph rather than by a customer, which is its own discipline and the subject of observability for a site that only breaks occasionally. Make those changes first and a surprising number of rewrite arguments dissolve on their own.
Four conditions that justify a rewrite
These are the conditions under which we would support starting again. They are narrow on purpose. One of them is usually enough; two make the decision obvious to everyone in the room without a slide deck.
- The substrate is ending and there is no upgrade path. A runtime out of support, a framework whose next major version was never released, a database engine the vendor has stopped patching, a licence that terminates on a known date. The test: can you reach a supported footing by upgrading in steps, with the product still shipping? If yes, that is an upgrade programme, not a rewrite.
- The data model contradicts how the business now works, at the root. Not an untidy schema. The root: one customer per account when customers now share accounts, one currency where there are eleven, an entity that used to be a status and is now an object with its own history. Every new feature pays a tax to route around it, and each route-around makes the next one more expensive.
- The system cannot be tested or observed, and cannot be made so incrementally. Some systems resist characterisation tests so completely that there is no safe first step: hidden global state, non-deterministic ordering, a build nobody can reproduce on a clean machine. This is rarer than it is claimed, and you should try for two weeks before you believe it.
- The product should be smaller than the software it is running on. Sometimes the correct rewrite is a deletion. The system grew around an ambition that was abandoned, and what remains would fit in a fraction of the surface. That is a product decision wearing engineering clothes, and it is the one case where the replacement is genuinely cheap.
Notice what is absent from that list. None of these conditions is about how the code reads, who wrote it, or whether the current stack is fashionable. The first one is the easiest to verify and the most often asserted without checking: an unsupported runtime accumulates published vulnerabilities that nobody will patch for you, and a dependency audit will tell you in an afternoon whether that has actually happened or is merely feared.
The fourth condition deserves a moment, because it is the one teams reach last. If the honest answer is that the organisation needs a presence rather than an application, the replacement may not be an application at all. Something like Nichevio, which assembles a mobile-first profile site from structured widgets, does a job that a second custom build would do worse and slower. The same reasoning drives the question of whether you need a native app, a web app, or neither, and it is worth asking before any budget is approved.
We cannot hire for this stack is a genuine problem and a poor reason to rewrite. It is a recruiting, training and contracting question first, and the market for any given language moves faster than a rewrite completes. Teams that rebuild for hiring reasons often finish into a market that has moved again, having spent the year not shipping.
The reasons that feel like reasons
Every one of these is heard weekly, and every one of them is a refactor, a measurement, or a management problem in disguise.
- The code is ugly. Ugly and working beats elegant and unfinished on every day of the year. Rename things, extract the worst function, put a test around the part that frightens you, and move on.
- We would do it differently now. Of course you would. You know things the earlier version could not have known. That is an argument for editing it, not for discarding the knowledge that is embedded in it.
- The framework is old. Old is not the same as unsupported, and unsupported is the only version of this that counts. Check the security posture and the upgrade route before you accept the premise.
- It is slow. Then profile it. A rewrite that skips the profile reproduces the same bottleneck in a new accent, because the bottleneck is usually a query, a payload size or a chatty boundary rather than the language.
- Nobody understands it. The most dangerous one, and the one that argues hardest against a rewrite. If nobody understands the system, nobody can specify its replacement, and you will discover what it did by breaking it.
- The team is demoralised. A real problem, and a rewrite is an expensive way to treat it. Morale usually recovers faster from shipping something visible in a fortnight than from a project whose first release is next year.
Second systems collect wishes
There is a failure pattern older than most of the stacks it ruins. Once a rewrite is approved, it becomes the place every deferred wish goes. The multi-tenancy somebody wanted. The event log. The plugin architecture. The internationalisation that was cut twice. Each addition is individually defensible, and together they turn a replacement into three projects sharing one budget and one deadline.
The mechanism is scope, and the rewrite is only the vehicle. Guard against it by writing down, before anything starts, what the new system will deliberately not do that the old one did. Some of what looks like debt was priced on purpose and should stay priced, which is the difference between debt worth keeping and debt to pay now.
Refactor, strangle, rewrite: what each one costs
The three options are usually compared on effort, which hides the differences that matter. Compare them on risk shape, on what happens to feature delivery while the work runs, and on what each one requires you to already have.
| Concern | Refactor in place | Strangler migration | Full rewrite |
|---|---|---|---|
| Time to first value | Days | Weeks | The whole programme |
| Delivery meanwhile | Continues | Continues, slower | Frozen or forked |
| Rollback | Revert a commit | Route traffic back | Abandon and apologise |
| Substrate change | None | One slice at a time | Total |
| Risk shape | Small and frequent | Medium and repeated | One large bet |
| Common failure | Never finishing | Two systems forever | Behaviour you forgot |
| Requires | Tests around the seam | A stable interface to cut on | A full specification |
| Honest use | Almost always | Substrate change with a live product | Ending substrate, or a smaller product |
Read the Requires row twice. A full rewrite needs a complete specification of current behaviour, and almost nobody has one. What teams have instead is a working system and a belief about what it does, and the gap between those two things is where rewrites go to die.
The strangler approach is the honest middle
Put a routing layer in front of the old system. Implement new behaviour in the new system behind that layer. Move one route across at a time, and delete old code when its last caller stops calling. The old system is never replaced by a launch. It is starved, slice by slice, while the product keeps shipping. For anything with real users this is the approach we would default to, and it is how most successful platform changes in web application development actually happen, whatever the retrospective blog post says.
Where to cut the first seam
- Choose a slice with a clear boundary and low coupling. A read-only surface, a report, a public page. Not the payment path, and not the thing everybody complains about, because the thing everybody complains about is coupled to everything.
- Measure the old slice before you move it. Response times, error rates, the business event it produces. Without that baseline you cannot tell whether the new version is better, worse or merely different.
- Move it behind a switch, and keep the old path warm. Run both through one full business cycle. Month-end, invoice runs and reconciliation are where assumptions surface, and they surface on a schedule you do not control.
- Delete the old path on a written date. This is the step teams skip, and skipping it is exactly how a migration becomes permanent. If the deletion has no owner and no date, it will not happen.
- Only then choose the next slice. The first crossing teaches you where the real coupling is. Planning slices three through nine before slice one has shipped is planning with fiction.
What strangling actually costs
Here is the concession. For the length of the migration you are running two systems, two deployment paths and two on-call surfaces, plus a routing layer that is itself a component capable of failing. You own the translation between two data shapes, and translation code is where subtle corruption hides. Anyone who calls this low risk has not run one to completion.
The characteristic failure is a migration that stops partway and stays there, because the remaining routes are the difficult ones and the attention moved elsewhere. It also puts weight on choices that resist being undone later, such as where the new slices run and what they depend on, which is the subject of hosting decisions that are hard to reverse. Decide those once, deliberately, at the first seam.
The risk nobody prices
Every rewrite plan prices engineering time. Almost none of them prices the three things that actually sink the work.
The corrections nobody wrote down
A system that has run for years is a sediment of corrections. The rounding rule that changed after a complaint. The retry that exists because a partner returns a success status with an error body. The date handling that quietly assumes one market and has been right ever since. None of it is in a specification, because there is no specification. It is in the code, and it is indistinguishable from mess unless you already know the story behind each line.
This is why nobody understands it argues against a rewrite rather than for one. The people who could explain why that strange condition exists have left, and the condition is load-bearing. Rewriting removes it, silently, and you find out when a customer who has been quiet for three years is suddenly billed wrongly.
A rewrite is not a cure for a system nobody understands. It is a bet that you understand it well enough to write it again.
The freeze, and who pays for it
While a rewrite is in flight, one of two things is true: the product stops changing, or every change is implemented twice. Both are expensive. The second is the one teams end up with without ever deciding to, and it doubles the cost of the smallest fix for as long as the programme runs.
Ask what the product is not allowed to do while the rewrite runs, then ask what that means for commitments already made to customers and to the sales conversation. If the answer is that nothing will be frozen because both systems will be maintained, price two teams and say so out loud. If the answer is that nothing important is due anyway, write that down and read it again in three months.
The third unpriced item is the set of contracts you did not know you had. URLs other people link to and bookmark. A webhook a partner registered once and forgot. An export that somebody in finance built a monthly process around. On the web this is a URL problem before it is anything else, and a rewrite breaks addresses the same way a replatform does, which is why the redirect map belongs in the plan on day one rather than the week before launch. The discipline is the same one that governs website migrations, and it is unforgiving of teams that treat it as cleanup.
How to decide in a week
You do not need a quarter of evaluation to answer this. You need one sentence and two weeks of evidence.
Start by writing the constraint in a single sentence, without naming a technology. Adding a second currency requires changing forty call sites is a constraint. The code is legacy is a mood. Then check that sentence against the four conditions above. If it matches none of them, what you have is a refactor with a scheduling problem, and the scheduling problem is the one to solve.
Then spend two weeks refactoring directly at the constraint rather than around the codebase in general. Not tidying. Aim at the exact thing your sentence describes, and see whether it moves. If it moves, it was never structural, and you have already banked the improvement instead of writing a proposal about it. If it refuses to move, you now have the strongest evidence any rewrite argument can carry, gathered cheaply and in public.
That two-week test settles most of these arguments, and it is the single practice we would add to any team that keeps having this debate. It is also the core of how we approach product improvement: establish what is genuinely immovable before anyone commits to moving it.
If you do rewrite, do these things
Suppose the evidence holds and the rewrite is real. Four practices separate the ones that land from the ones that quietly become a second maintained system.
Treat the old system as the specification, because it is. Write characterisation tests against its actual behaviour, including the parts that look wrong, and run the same suite against the replacement. Where the two disagree, decide deliberately which one is correct rather than assuming the new one is. Then get the new system in front of a small slice of real traffic long before it is finished, because a replacement that has only ever been correct in staging has not been tested at all.
Keep the old system runnable through one complete accounting cycle after cutover, not one week. And agree a stop rule before the first commit: the condition under which you abandon the attempt and go back. Programmes without a stop rule finish only because stopping became embarrassing, which is the most expensive reason to finish anything.
The reverse case, stated honestly: if your product is young, small and still changing shape every week, none of this applies. Rewriting a codebase with few users is cheap and often the fastest way to learn what you were actually building. The cost of a rewrite scales with how much of the business has grown into the software, which means the real question underneath the technical one is what the organisation is selling and which part of the code carries it. Our parent company argues that case directly in what a company actually sells, and it is a better starting point than any framework comparison.
If you are holding a six-month proposal right now, do the two-week test first. It costs a fortnight, it produces evidence either way, and it is the cheapest thing in this entire argument. If you would like a second opinion on a specific system before that fortnight starts, tell us what it does and what it will not let you change.
Common questions.
When is a rewrite better than a refactor?
A rewrite is better when the constraint sits below the code and cannot be reached by editing it. That means an unsupported runtime with no upgrade path, a data model that contradicts how the business now works at its root, or a system that resists testing so completely that no safe incremental step exists. If the blocker can be named and then moved by two weeks of focused refactoring, it was not structural.
What is the strangler pattern in software migration?
The strangler pattern replaces a system gradually rather than all at once. A routing layer sits in front of the old system, new behaviour is built behind it in the new system, and traffic moves across one slice at a time until the old code has no callers left and can be deleted. The product keeps shipping throughout, and each step can be routed back if it goes wrong.
Should we rewrite because we cannot hire for our current stack?
No, hiring difficulty alone does not justify a rewrite. It is a recruiting, training and contracting problem, and each of those options costs far less than rebuilding a working system. Technology demand also shifts faster than a rewrite completes, so teams that rebuild for hiring reasons often arrive in a market that has moved again, having spent the intervening period not shipping product.
What is the biggest hidden cost of a software rewrite?
The behaviour that exists only in the old code. Years of small corrections, edge-case handling and workarounds for other systems accumulate without ever being documented, and they are indistinguishable from untidy code unless you know why each one was added. Rewriting removes them silently. The second hidden cost is the feature freeze, or the doubled work of maintaining two systems at once.
Can you refactor a system nobody on the team understands?
Yes, and it is safer than rewriting one. Start by adding tests that capture what the system currently does, including behaviour that looks wrong, then change small pieces and confirm the tests still pass. Understanding grows as coverage grows. A rewrite requires you to specify the replacement up front, which is impossible when nobody can explain what the original does.
How do you stop an incremental migration running forever?
Give every moved slice a deletion date and an owner for the old path. Migrations become permanent when the new route ships, the old route stays switched on as a safety net, and nobody is accountable for turning it off. Track the number of routes still served by the old system as a visible figure, and treat the final difficult slices as the plan rather than as leftovers.
Facing this in your
own business?
Tell us where you’re headed — we’ll map the shortest honest route.