'We need to rewrite it' is one of the most expensive things a CTO can say. The history of big-bang software rewrites is a long list of projects that took twice as long, cost three times as much, and delivered less than the system they replaced. Joel Spolsky's 2000 essay 'Things You Should Never Do' documented this pattern — and teams have been ignoring the warning ever since.
This post is about the alternative: how to modernize a legacy application incrementally, reducing risk at each step, while keeping the existing system in production and generating value throughout the process.
Why 'just rewrite it' almost always fails
Legacy systems are ugly because they encode years of business requirements — many of them implicit, undocumented, and only discoverable by running the code. When you rewrite from scratch, you start by throwing away all of that accumulated knowledge. Then you spend the first year reproducing it, imperfectly, without realizing what you've lost.
The new system is also being built by engineers who have never used the old system in anger. The legacy code looks like spaghetti to them, but each tangle often represents a real edge case that a customer hit once. Rewrites routinely ship missing these cases — and discover them through production incidents.
The 6 Rs framework: choosing your modernization approach
Not every legacy system needs the same treatment. The 6 Rs framework gives you a vocabulary for the options:
| Strategy | Description | When to use |
|---|---|---|
| Retain | Keep the system as-is | The cost of change outweighs the benefit; system is stable |
| Retire | Decommission the system | Business process no longer needed; duplication with another system |
| Rehost | Lift and shift to new infrastructure | Need to move off legacy hosting without changing the application |
| Replatform | Minor optimizations while moving | Modernize the infrastructure layer without touching core code |
| Refactor | Improve the code without changing behavior | System is maintainable but technical debt is slowing velocity |
| Rebuild | Rewrite a component from scratch | A specific component is too costly to refactor; scope is contained |
Notice that 'full rewrite' doesn't appear as a clean option. Rebuild applies to specific components, not entire systems. The full rewrite scenario is usually a sign that the other options haven't been seriously evaluated.
The strangler fig pattern in practice
The strangler fig pattern — named after a tree that grows around a host and gradually replaces it — is the most proven approach to incremental legacy replacement. The sequence:
- Keep the legacy system running — don't touch it at first. It's generating revenue and serving users.
- Build new functionality as services — new features go into new services, not the legacy system. The legacy system doesn't grow.
- Migrate existing functionality incrementally — move specific capabilities from the legacy system to new services, one at a time.
- Redirect traffic — use a facade or API gateway to route requests to either the legacy system or the new services depending on which handles that capability.
- Retire the legacy system — once all functionality has been migrated, the legacy system can be decommissioned.
The key property of this pattern: at every stage, you have a working system. There's no period where nothing works. Rollback is always possible because the legacy system continues to run until you're ready to retire it.
Starting with the API layer
The practical first step for most legacy modernization projects is to isolate the legacy system behind a clean API facade. You build a new API layer in front of the legacy system that translates modern API calls into whatever the legacy system understands.
This gives you several immediate benefits: the rest of your application now talks to a modern, versioned API rather than directly to the legacy system; you can change the legacy system's internals without touching every integration point; and when you're ready to replace the legacy system, only the API layer needs to change.
What success looks like at 3, 6, and 12 months
3 months: The legacy system is behind an API facade. New feature development is happening in new services, not the legacy codebase. The team has built confidence with the deployment pipeline for new services.
6 months: Two or three significant capabilities have been migrated to new services. The legacy system is smaller. The team can point to specific metrics that have improved: deployment frequency, incident rate, development velocity for the migrated capabilities.
12 months: The legacy system's surface area is meaningfully smaller. New engineers can contribute to the modernized components without understanding the full legacy system. A clear retirement date for the legacy system is visible on the roadmap.