Back to blog
Custom Software

Rewrite or Refactor? How to Modernize a Legacy System Without Betting the Company

Full rewrites fail more often than anyone admits. When incremental modernization wins, when a rewrite is truly unavoidable, and how the strangler-fig approach replaces a system piece by piece while the business keeps running.

Somewhere in your company there is a system nobody wants to touch. It runs payroll, or billing, or the core of the product. The people who wrote it left years ago. Every change takes three weeks and breaks something unrelated. And every few months someone proposes the obvious fix: throw it out and rewrite it clean.

That instinct is where a lot of companies quietly lose a year of engineering and a good chunk of their runway. The rewrite is rarely the safe option it looks like. Most of the time the boring path — modernizing in place, piece by piece — gets you further with less risk. But not always. The trick is knowing which situation you're actually in before you commit.

Why the clean-slate rewrite fails so often

The problem isn't discipline. It's that a legacy system encodes years of decisions you can't see. Every weird conditional, every special case in the tax logic, every retry someone added at 2 a.m. — that's a bug that once hit a real customer. A rewrite starts from a blank file and has to rediscover all of it, usually in production, usually the hard way.

There's also the parity trap. While your new system spends 18 months catching up to what the old one already does, the old one keeps shipping features the business asked for. You're chasing a moving target with a smaller team. Rewriting from scratch gets called the single worst strategic mistake for a reason: you throw away working knowledge to buy a cleaner codebase nobody is paying you for.

  • "The code is a mess" is the only reason anyone can state
  • Nobody can describe what the current system does end to end
  • The estimate is in months, and no one has written down what "done" means
  • The plan includes freezing new features during the rewrite

The knowledge you delete when you delete the code

Old code is documentation that happens to run. Before you declare it worthless, extract what it knows: which edge cases it handles, which integrations it talks to, what the real data looks like versus what the schema claims. Teams routinely find that 20-30% of a "simple" legacy module is handling cases the new design never accounted for.

  • Business rules that live nowhere else (regulatory, tax, pricing tiers)
  • Undocumented integrations and the exact quirks of each one
  • Data that violates its own schema — nulls, sentinel values, legacy encodings
  • Performance workarounds that look ugly but exist for a reason

When modernizing in place is the right call

For most business systems that still work, incremental modernization wins. You keep the thing running and earning while you improve it. Risk stays small because each change is small and reversible. This is the default; you should need a specific reason to leave it.

  • The system works and the business depends on it every day
  • The architecture is sound even if the code is dated (a monolith is not automatically the enemy)
  • You can upgrade the runtime, framework, or database version incrementally
  • The pain is concentrated in a few modules, not spread everywhere

When a rewrite is genuinely the honest answer

Sometimes incremental is a fantasy. If the platform underneath is dead — a framework with no security patches, a language runtime that's end-of-life, a database version the vendor stopped supporting — you're not modernizing, you're doing life support. At some point the risk of staying beats the risk of moving.

  • The core platform is out of support and can't be upgraded in place
  • You literally cannot hire people willing to work on it
  • The architecture blocks something the business now requires (multi-tenant, horizontal scale) and no amount of patching adds it
  • Change cost has grown so high the old system already fails to ship

The strangler fig, in practice

When you do need to replace a system, you rarely need to replace all of it at once. The strangler-fig pattern (named by Martin Fowler after a vine that grows around a tree until it stands on its own) replaces the old system one capability at a time, with both running in parallel.

You put a routing layer — usually a proxy or an API gateway — in front of the old system. New code for a given feature lives behind that router; requests for that feature get sent to the new implementation, and everything else still hits the legacy one. Piece by piece, traffic moves over. The old system shrinks until there's nothing left worth keeping, and you turn it off.

  • Put a facade in front so callers don't know or care which side answers
  • Pick the first slice by value and low coupling, not by what's most fun to build
  • Migrate one capability end to end, data included, before starting the next
  • Keep both systems in sync during the transition, then decommission deliberately

The plumbing that keeps it safe

The pattern only works if you can move traffic without holding your breath. That means investing in the boring infrastructure first: characterization tests that pin down what the old system does today (right or wrong), feature flags to shift traffic gradually, and an anti-corruption layer so the new code isn't forced to inherit the old data model. Observability on both sides tells you the moment a migrated slice behaves differently.

  • Characterization tests to capture current behavior before you change it
  • Feature flags and canary routing to move 1%, then 10%, then 100%
  • An anti-corruption layer between the old and new data models
  • Metrics and logging on both paths so you catch drift immediately
  • A tested rollback for every slice — if you can't roll back, you're gambling

A decision you can defend

Before anyone writes a line of new code, answer these honestly. If the system works and the platform is alive, modernize in place. If the platform is dead or the architecture blocks the business, plan a rewrite — but run it as a strangler, one slice at a time, never as a big-bang freeze-and-replace. The goal isn't the prettiest codebase. It's to end up modern without a gap where the business stopped running.

  • Can we name exactly what the current system does, or are we guessing?
  • Is the platform underneath still supported, or are we on borrowed time?
  • Can we ship one slice to production in weeks, instead of the whole thing in a year?
  • Do we have tests and rollback for each step, or are we betting the company on one cutover?

Want help applying this to your system?

Book a call