// software
How to modernize a legacy system without rewriting it
The instinct when facing a legacy system is to replace it: the code is frightening, the framework is end-of-life, and a fresh start feels like the professional choice. It is also, in our experience and in the software literature going back to Brooks, the single most reliable way to spend two years and end up with two bad systems.
There is a better default, and it is older than most of the teams asking us about it. This guide covers why rewrites fail, the strangler fig pattern that replaces a system piece by piece, and the narrow set of situations where starting over really is the right call.
Why the big-bang rewrite fails
A rewrite asks you to rebuild behaviour that took years to accumulate — edge cases encoded in code nobody remembers writing, requirements that exist only as behaviour, integrations whose original purpose was never documented. Meanwhile the old system must keep running, so you maintain two codebases with one team, and the new one stays "almost ready" while reality diverges from the specification you froze on day one. Brooks called the overconfidence behind this the second-system effect: the team that struggled with the modest first system designs a grand second one, and ships it late, over budget and over-engineered. The failure is not laziness or weak engineers. It is the assumption that behaviour can be re-derived from scratch, when the running system is the only place the behaviour survives.
The strangler fig pattern, concretely
The alternative, named by Martin Fowler after the fig trees that grow around a host and eventually replace it: put the new system in front of the old, route one narrow slice of traffic to the new code, and let everything else fall through to the legacy system untouched. Then take the next slice, and the next. Each release is small and reversible, the old system keeps working throughout, and the team learns the domain incrementally instead of discovering requirements in year two. There is no cutover weekend on which everything must work at once — only a long series of ordinary releases, each making the new system thicker and the old one thinner, until nothing routes to the legacy system any more and it can be retired quietly.
Finding the seams and cutting the first slice
Success depends on where you cut. Start with seam identification: find the natural boundaries in the old system — a module with few callers, a flow that touches few tables, a feature whose inputs and outputs are simple to describe. Between old and new, build an anti-corruption layer: a translation boundary that lets each side speak its own model and keeps legacy vocabulary from leaking into the new code. Then choose the first slice by value, not by shame — the most painful change, the riskiest manual step, the part the business most wants improved. Keep the perimeter deliberately small: one flow, a few weeks of work, demonstrably better. Momentum in the first months matters more than architecture in year three.
When a rewrite actually is justified
Two conditions make starting over the honest choice. Technology with no maintainers: the platform is abandoned, the runtimes are disappearing, and the pool of people who can operate it shrinks every year — no pattern wraps around a foundation that will not compile. Security constraints that cannot be met: the architecture cannot support what the business now requires, and bolting it on would mean rebuilding the core anyway. Absent from the list: ugly code, an unfashionable framework, a new team finding the style alien. Whatever you build, keep the rule that saves every migration: the old system is the specification. Migrate behaviour, validated output by validated output, not structure — and test against what the old system actually does, the only specification that was ever accurate.
Frequently asked questions
- How long does a strangler fig migration take?
- Honestly: quarters to years, proportional to the size of the legacy system. The difference from a rewrite is that value arrives from the first slice and every slice after, the business is never hostage to a cutover date, and stopping halfway still leaves you better off. A rewrite that stops halfway leaves you with nothing.
- What is an anti-corruption layer?
- A translation boundary between the legacy system and the new one: the new code speaks its own domain model, and the layer converts to and from the legacy model at the edge. It keeps outdated terminology, data shapes and assumptions from infecting the new codebase, and it means the legacy side can change without rippling through everything you have built.
- Can we modernize and add new features at the same time?
- Yes — that is one of the pattern's real advantages. New features go into the new system from day one, which means the modernization is funded by capability the business wants, not only by technical debt reduction. Teams that pause feature work for a "migration phase" lose their mandate; teams that deliver features while migrating keep it.
// next steps
Working on something adjacent? Tell us about it — or read the services we deliver today.