Skip to content
Epic Software Labs
All articles

Risk

When to rewrite vs refactor

Rewrites fail more often than teams expect, for predictable reasons. How to spot the rare case where one is right, and how to replace a system incrementally.

9 min readEpic Software Labs

Key takeaways

  • The instinct to rewrite is usually about not understanding the existing system rather than the system being unfixable.
  • Rewrites fail because the old system keeps changing while the new one is built, so you are chasing a moving target with half your capacity.
  • Incremental replacement — routing traffic piece by piece from old to new — gets most of the benefit with a fraction of the risk.
  • A genuine rewrite case exists when the platform is unsupported, the data model is fundamentally wrong, or the system cannot meet a hard requirement it now has.
  • If you do rewrite, freeze the old system's feature set first. A rewrite against a moving target is the standard way these fail.

The rewrite vs refactor decision comes up at some point on every engineering team. The argument is always coherent: the current system is a mess, progress has slowed, and a clean start with what we know now would be faster in the long run.

It is occasionally correct. It is more often the most expensive mistake available, and the reasons are well enough understood to check against.

Why the instinct is unreliable

Reading unfamiliar code is much harder than writing new code. This produces a persistent illusion: the existing system looks worse than it is, because the reader does not know why each strange piece exists.

Most of those strange pieces are not mess. They are encoded reality — the bug fix for the customer whose data had an unexpected shape, the workaround for the third-party API that returns an error on Sundays, the special case for the enterprise account that pays for a quarter of your revenue.

A rewrite discards all of it, and rediscovers it through production incidents over the following year.

The structural reason rewrites fail

The decisive problem is not technical. It is that the business does not stop.

While you rewrite, the old system keeps accruing bug fixes, compliance changes and features that customers are paying for. So you are building toward a target that moves, with a team split across both. The new system is perpetually a few months from parity, and "a few months from parity" is a state that can persist for years.

Meanwhile nothing ships to customers, which becomes progressively harder to defend.

Incremental replacement instead

The pattern that works is to replace pieces while the system keeps running — Fowler's strangler fig. Build the new component alongside the old, route the relevant traffic to it, verify, move on.

  • Each increment delivers value on its own
  • Each can be reversed independently if it goes wrong
  • The system is working throughout
  • You can stop at any point and still be better off
  • Learning from the first piece informs the second

It is slower in total than an idealised rewrite. It is faster than a real one, and it does not require anyone to hold their nerve for eighteen months.

A worked strangler fig migration

The pattern described above stays abstract without a real shape attached, so here is one, run against a common case: an ageing monolithic order management system that a growing retailer needs to modernise without stopping order processing for a single day.

The starting state. A ten-year-old PHP monolith handling everything — order intake, inventory, fulfilment routing, customer notifications, reporting — on a database schema that grew organically and now has workarounds layered on workarounds, particularly around a multi-warehouse feature bolted on years after the original single-warehouse design.

Increment one: fulfilment routing, isolated first. Not because it is the most valuable piece, but because it is the most self-contained — it reads order and inventory state and writes a routing decision, with a clear boundary. A new service is built alongside the monolith, and a routing rule sends 5% of new orders through it, with the monolith still processing the other 95% and acting as the fallback if the new service errors. Two weeks of real traffic at 5% surfaces three edge cases in multi-warehouse orders the team had not fully specified, caught while the blast radius was one order in twenty rather than every order.

Increment two: ramp and cut over. Once the new service handles the edge cases correctly, traffic ramps to 25%, then 100%, over three more weeks, with the ability to route back to the monolith at any point if something goes wrong — which it does, briefly, in week two, when a warehouse capacity edge case appears that the 5% sample had not included. Rolling back costs an afternoon, not a crisis, precisely because the monolith never stopped being able to handle the traffic.

Increment three: inventory, six weeks later. The next self-contained piece, following the same pattern — build alongside, route a slice of traffic, ramp, cut over, with the old system always able to take load back. By the time inventory is fully migrated, the team has done this three times and the process itself has sped up, because the pattern for standing up a new service alongside the monolith is now well understood.

What did not get touched. Reporting and customer notifications stayed on the monolith for over a year, because neither was causing enough pain to justify the migration effort — a decision the team revisited only once notifications needed a capability the old system genuinely could not support. This is the incremental approach working as intended: pieces migrate when there is a real reason to migrate them, not according to a plan that assumed everything needed to move.

Total elapsed time for the pieces that did migrate: roughly eight months, spread across a team that continued shipping ordinary feature work throughout, rather than the eighteen-to-twenty-four-month blackout a full rewrite of the same scope would likely have required — with zero periods where order processing was unavailable, because the old system was never turned off until its replacement had already proven itself under real load.

Legacy system modernisation without a rewrite at all

Not every legacy system problem needs even an incremental migration — worth naming, because legacy system modernisation gets reflexively equated with either a rewrite or a strangler fig migration, when a narrower intervention sometimes solves the actual pain.

If the problem is specifically that the system is hard to deploy, modernising the deployment pipeline around an otherwise-working codebase can resolve most of the felt pain without touching the application code at all. If the problem is that a small number of dependencies are dangerously outdated, a focused dependency upgrade project — real work, but bounded and well-understood — addresses the actual risk without the scope of a migration. If the problem is that nobody understands the system, investing in documentation and a deliberate knowledge-transfer effort can resolve the bus-factor risk described in our technical due diligence checklist without any code change at all.

The pattern worth internalising: name the specific pain before reaching for rewrite or migration as the default answer. "The system is old" is not a specific pain. "Deploys take four hours and require one person" is, and it has a narrower, cheaper fix than either a rewrite or a full incremental migration.

When a rewrite is genuinely right

The narrow set of cases:

The platform is unsupported. A runtime with no security patches, a database version past end of life. This is a hard deadline, not a preference.

The data model cannot express the business. If every new feature requires a workaround because the fundamental shape is wrong, incremental change genuinely cannot fix it — you are patching around the foundation.

A hard requirement cannot be met. A regulatory obligation, a security posture or a performance floor the architecture cannot reach. Note the word cannot; "would be difficult" does not qualify.

It is small. If a full rewrite is three weeks, do whichever you prefer. The risk of a small rewrite is small.

What is not on this list: the team dislikes the language, a newer framework exists, or the code is ugly. Those are real feelings and they are not business cases.

If you do it, do it properly

Freeze the old system. Critical fixes only. Building against a moving target is the standard failure, and the freeze is the single most important decision.

Feature parity and nothing else. Every improvement gets deferred to after the switch. Scope expansion during a rewrite is how eighteen months becomes three years.

Ship in slices. Even a rewrite can be migrated incrementally — take real traffic on part of it as early as possible. A cutover that has never carried production load is an unbounded risk.

Keep the old system running until you are certain. The ability to route back is worth the cost of maintaining both.

Set a reassessment date. A point at which you will honestly evaluate whether to continue. Sunk cost is at its most persuasive halfway through a rewrite.

The question to ask first

Before approving either: what specific thing will we be able to do afterwards that we cannot do now?

If the answer is a concrete capability — meet a regulation, support a volume, ship a feature that is currently impossible — you have a business case, and you can size it.

If the answer is "development will be faster", ask for the number. How much faster, measured how, and how long until the rewrite has paid back the year it cost? That question resolves most rewrite proposals on its own.

Should we rewrite our software: a decision checklist

Distilling the whole piece into a short checklist for the actual moment someone proposes it.

Ask these in order, and stop as soon as one gives a clear answer:

  1. Is the platform itself unsupported — no security patches available, a database or runtime past end of life? If yes, this is a hard constraint, not a judgement call, and some form of migration is required regardless of preference.
  2. Does the data model fundamentally fight the business, such that every new feature requires a workaround rather than a natural extension? If yes, incremental refactoring genuinely cannot fix a wrong foundation, and a rewrite — likely via the strangler fig pattern rather than a single cutover — is justified.
  3. Is there a hard requirement the current architecture cannot meet — a regulatory obligation, a security posture, a performance floor? If yes and it is genuinely a "cannot," not a "would be expensive to," that is a business case.
  4. Is the system small enough that a rewrite is genuinely a matter of weeks? If yes, the risk is proportionately small and either choice is reasonable.
  5. If none of the above apply, the proposal is very likely the instinct described at the top of this piece — unfamiliarity read as inadequacy — and the better response is incremental improvement, informed by the specific, named pain rather than a general sense that the system is old.

A big bang rewrite — replacing the whole system at a single cutover rather than incrementally — should be reserved for cases where the system is small enough that the cutover risk is genuinely bounded, or where regulatory or platform deadlines leave no time for an incremental approach. Outside those cases, the incremental pattern described earlier in this piece consistently outperforms it, for the structural reason already covered: the business does not pause to let a rewrite catch up.

Whichever path you take, the timeline implications are worth planning for honestly rather than optimistically — how long does software take to build covers the estimation traps that apply with particular force to migration and rewrite projects, where the unknowns are usually larger than on greenfield work.

The instinct is worth interrogating, not silencing

None of this means the instinct to rewrite is always wrong — it means it deserves scrutiny before being acted on, using the checklist above rather than the immediate feeling that a fresh start would be simpler. Engineers who raise the idea are usually pointing at something real, even when a full rewrite is not the right response to it; the useful move is finding the specific, nameable pain underneath the general instinct, and addressing that directly rather than reaching for the most dramatic available fix.

In one sentence

Most rewrite instincts are really requests for better documentation and a fresh set of eyes on a system nobody currently understands — address that first, and the case for a full rewrite usually weakens considerably.

A practical next step

The next time a rewrite gets proposed, ask the person proposing it to name the single feature that would become possible afterward that is not possible today. If they can name one concretely, you have the beginning of a real business case worth sizing properly. If the answer is some version of "everything would be easier," that is the unfamiliarity instinct talking, and the better next step is a week spent documenting the existing system before any decision about rewriting it gets made.

The question that outlives any specific decision

Whatever a team decides about a specific system, the underlying question is worth asking routinely rather than only when a rewrite gets proposed: could a newcomer understand why this part of the system looks the way it does, from the code and documentation alone, without asking someone who was there when it was built? A yes to that question, maintained consistently, is what actually prevents the unfamiliarity-driven rewrite instinct from arising in the first place.

One more thing worth saying

Most systems that look like they need a rewrite actually need someone to understand them properly first. That is a smaller, cheaper, and far less risky project than it sounds.

Frequently asked questions

Should we rewrite our legacy system?

Usually not, and the instinct is worth interrogating before acting on it. Most rewrite proposals come from unfamiliarity with the existing system rather than from a specific defect in it — the code looks worse than it is because the reader does not know why each odd thing is there. Genuine cases exist, but they are narrower than they feel: an unsupported platform, a data model that cannot express the current business, or a hard requirement the architecture cannot meet.

Why do software rewrites fail so often?

Mainly because the business does not stop while you rewrite. The old system continues accruing fixes and features, so the new one is chasing a moving target with a fraction of the team. Meanwhile all the accumulated edge-case handling in the old code — which looks like mess but is usually years of encoded reality — gets lost and has to be rediscovered through production incidents.

What is incremental replacement?

Building the new system alongside the old one and moving functionality across piece by piece, with traffic routed to whichever handles a given path. Each increment delivers value and can be reversed independently, so the risk is bounded at every step. It takes longer in total than an idealised rewrite and succeeds far more often than a real one.

When is a full rewrite actually justified?

When the platform is genuinely unsupported and cannot be patched; when the data model cannot represent what the business now does, so every feature requires a workaround; when a hard requirement — regulatory, security, performance — cannot be met by the current architecture; or when the system is small enough that a rewrite is a matter of weeks. Note that 'the team dislikes the technology' is not on that list.

How do we stop a rewrite from dragging on?

Freeze the old system to critical fixes only, so you are not building against a moving target. Set a hard scope: feature parity and nothing more, with every improvement deferred to after the switch. Ship the new system to real traffic in slices rather than in one cutover. And set a date at which you will stop and reassess rather than continuing on sunk cost.

What is the strangler fig pattern in software migration?

A migration approach, named after the strangler fig plant that grows around a host tree, where a new system is built alongside an old one and gradually takes over its responsibilities piece by piece, with traffic routed to whichever system currently handles a given function. The old system keeps running throughout, providing a fallback at every stage, until the new system has fully replaced it and the old one can finally be retired. It trades a longer total timeline for dramatically lower risk compared to a single cutover rewrite.

What is a big bang rewrite and why does it usually fail?

A big bang rewrite replaces an entire system in one release, with the old system switched off and the new one switched on at a single cutover point, rather than migrating incrementally. It fails more often than incremental alternatives because the business does not pause while the rewrite happens — the old system keeps accumulating changes that the new one has to match, the team is split across old and new, and any gap between the two systems' behaviour only becomes visible at the single, high-stakes cutover moment rather than being caught incrementally along the way.

References & further reading

  1. [1]
  2. [2]
  3. [3]