Working With Legacy Code: Don't Fall Into the Rewrite Trap
Every senior engineer faces legacy code repeatedly. "Let's rewrite" is tempting and dangerous. Strangler fig, test-before-refactor, evolving in small steps — lessons from years of practice.
You start a new job. First day, open the codebase. Code from 2014, six different devs touched it, no docs, no tests or broken ones. Your first instinct: "Let's rewrite this."
Resist that instinct. Rewriting is one of the worst decisions in software engineering — for the great majority of cases. This piece walks through the alternatives and the practices that actually work.
Why rewriting is a trap
Joel Spolsky's famous line: "The single worst strategic mistake a software company can make is to rewrite the code from scratch."
Reasons:
1. Old code holds invisible knowledge. That code accumulated patches over years. Each patch was a bug fix, an edge case, a customer request. Rewriting throws that knowledge away. The new code starts producing the same bugs six months later.
2. "Writing new code is easier than reading old code." A myth. Rewrites estimated at 2 months take 12. The first 6 months the new code lacks full functionality, you maintain two systems in parallel, double the work.
3. Product stalls during the rewrite. 80% of the team is invested in the new system, the old one only gets bug fixes. Customers see no new features. Competitors move ahead.
4. The result feels "modern" but contains the same problems. The same architectural mistakes get rebuilt because the team didn't articulate them before starting.
The strangler fig pattern
Instead of rewriting: gradual evolution. The pattern is Martin Fowler's strangler fig.
Concept: build the new system around the old. The new system gradually takes over jobs from the old. The old fades into irrelevance. There's never a "big rewrite" moment.
Concrete steps:
1. Put a gateway / proxy in front. Route all requests through one entry point. It points to the old system at first.
2. Reimplement one feature in new code. A single endpoint, a single flow. Configure the gateway to route that feature to the new code. The old code is still there but receiving no traffic.
3. Test, validate, deploy. The new feature is live. The old code stays (for safety) but isn't used.
4. Delete the old code. Once traffic is verified at zero, the old code is removed.
5. Pick the next feature, repeat.
This takes 6 months to 2 years, but:
- Zero "down day"
- Each step pays for itself (new feature live)
- Errors get rolled back step-by-step
- The team gets used to the new system while the old still works
Test-before-refactor
If you want to safely refactor a block of code, write tests first. Tests that capture the current behavior.
1. Write tests covering the current behavior (they pass on the old code)
2. Refactor
3. Tests still pass? Refactor was safe
Without tests, refactor is hope-based code change. With them, you can evolve legacy code for years.
Boy Scout rule
Robert C. Martin's simple rule: "Leave the campsite cleaner than you found it."
You're writing a new feature; the file you're touching has old code. Time to refactor. With caveats:
- Only what you touched. Don't rewrite the whole file.
- With tests. Every change verified.
- Don't expand PR scope. "Refactor + new feature" PR = hard to review, risky. Refactor first, then feature.
This discipline produces dramatic differences over 6–12 months. The codebase a year later is much cleaner — without anyone saying "let's rewrite."
The Ship of Theseus problem
If every part of a ship gets replaced over time, is it still the same ship? A philosophical question with practical software implications.
A codebase under strangler fig + Boy Scout rule is unrecognizable two years later. There may not be a single line of the old version left. But there's no "rewrite" — every day was a small evolutionary step.
This gives you all the upside of a rewrite without any of the downside.
When rewriting is actually correct
Rare but real scenarios:
-
The system is dying: the language/framework no longer has support, no security patches, still running but at risk of explosion within 6 months. Evolution isn't enough; parallel construction is needed.
-
You need a fundamentally different architecture: monolith → distributed (e.g., a cloud-native transformation). Strangler fig is used, but parallel construction is unavoidable.
-
Very small system: a 1000-line tool. Rewriting it takes a week. Re-testing takes the same. Risk is low. Rewrite is fine.
Otherwise: evolve.
The human side
Legacy code is the result of years of work. The people who wrote it might still be on the team and proud of it. "This code is bad" needs to be said with a professional tone:
- "This code was written 6 years ago in different conditions" (context, not blame)
- "We want to adapt it to current needs" (transform, not destroy)
- "We're learning from past decisions, building on them" (respect)
This tone is critical to team culture. If you want to work with the same teams for 5 years, learn to handle legacy without public-shaming anyone.
Closing
Legacy code is the silent norm of the industry. You'll meet it on day one of your new job; you'll still be living with it five years later (in a transformed form).
Before saying "let's rewrite," try: strangler fig + test-before-refactor + Boy Scout rule. A team that practices these for a year will navigate legacy comfortably.
Rewriting is the last resort. Evolution first. Most of the time, evolution is enough — and it's far cheaper than rewriting.
Long-form notes on distributed systems, production stability, AI-assisted shipping. No spam, easy to unsubscribe.
Comments
No comments yet. Be the first.