Taking Over Legacy Systems: Stabilize Before Rebuilding
Inherited codebase, developer gone, no docs? The order that works: run it, back it up, test it, then rebuild the worst part.
For Business

On this page
A legacy system is still in use but difficult to change or maintain. A client calls with one that already works, mostly. The person who built it is gone — moved on, unreachable, or never wrote anything down. No documentation, no test suite, no deployment runbook.
The client doesn't want a lecture on technical debt. They want it running, by next week.
This is the rescue engagement, with its own discipline, separate from greenfield work and normal maintenance. The temptation, from the client and sometimes the engineer, is to start "fixing" things immediately: refactor the ugly parts, upgrade the stack, maybe rewrite it properly. Making those changes before understanding the system is risky. You don't yet know what the system actually does, so you can't tell an improvement from a break.
Step zero: make it run before you judge it
Before opening a single file with intent to change it, get the system running on a machine you control. Not "it works in production, trust the client's word for it" — clone it, install the dependencies at the versions actually used, use authorised representative test data in an isolated environment, and watch it boot.
This surfaces the first honest inventory of the engagement: which environment variables are required, and which scheduled jobs nobody mentioned. It also turns up which API keys are hardcoded into an old config file. Code and configuration review can identify some issues; isolated execution shows actual behaviour.
Skip this step and every later decision becomes guesswork: you can't safely change what you can't run.
Backups and monitoring, before features
Once the system runs locally, the next move isn't a feature. It's making sure production can survive the next mistake. A system run unattended for years, with no documented owner, is one where nobody knows if backups are even happening, let alone whether they'd restore.
Two things go in immediately, before any functional change:
- Verified backups. Not a cron job running
pg_dumpinto a folder nobody checks — an actual, tested restore. A dump file needs a tested restore before you can rely on it for recovery. - Monitoring and alerting. Uptime, error rates, disk space, queue depth — whatever the system's real failure modes are. If it goes down at 2 a.m., someone should know before the client calls angry.
This ordering is non-negotiable. It's the one place where skipping it has unbounded cost. A missing feature costs a sprint. A data-loss incident on a system you just inherited costs the client, and possibly your reputation with everyone else.
Characterization tests: capture what it does, not what it should do
With the system running and protected, the next question is how to change it safely. Normal test-driven development assumes you know the intended behavior. On an inherited system with no docs, you often don't. The "intended" behavior is whatever the code currently does, quirks included, because users have depended on those quirks for years.
Michael Feathers calls the answer a characterization test: a test that documents current behavior instead of checking it against a specification. As he puts it, "instead of trying to figure out whether code is correct or not, we can try to characterize its behavior to understand what it actually does." Current production behavior is an important reference, though it may include defects that need a separate decision to correct.
Pick the two or three flows the business depends on — checkout, invoice generation, the nightly reconciliation job — and write tests pinning down their current output. Feathers' definition of legacy code isn't "old code." It's code without tests, and a characterization test puts a floor under code you don't yet trust.
Strangler fig: replace the worst part, not the whole thing
With backups, monitoring, and a characterization net in place, you can start improving things — but not by rewriting. The pattern that fits here is what Martin Fowler named the strangler fig application, after the vine that gradually envelops and replaces its host tree. New functionality is built alongside the legacy system, and a growing share of traffic is routed to it through a facade. Old code is retired once the new path proves itself.
Fowler's case for this over a rewrite is about risk: "the reduced risk and earlier value from the gradual approach outweigh its costs," even though the facade is scaffolding you'll eventually throw away. It costs more engineering hours than a clean rewrite looks like on paper. It's worth it because the business can continue using the system while parts are replaced.
Pick the worst subsystem first — the one causing the most incidents — and strangle that one. Leave everything that already works alone.
Why "let's just rewrite it" is usually the expensive option
Every rescue engagement eventually produces a client who asks: why not just start over? The honest answer is that a full rewrite is almost always the most expensive item on the table, and the risk compounds with the system's age.
Joel Spolsky's account of Netscape rewriting Navigator from scratch is the canonical case study. He called it "the single worst strategic mistake that any software company can make." The rewrite took roughly three years, while competitors gained market share.
His core argument: "when you throw away code and start from scratch, you are throwing away all that knowledge. All those collected bug fixes. Years of programming work." Ugly legacy code is often ugly because it encodes a fix for a real incident nobody remembers. A rewrite throws that away, and hands competitors, in Spolsky's words, "a gift of two or three years."
When a full rewrite actually is correct
A narrow case does exist, worth naming so it isn't confused with the tempting-but-wrong one. A rewrite can be appropriate when the platform cannot be maintained safely — for example, a runtime with no security patches and no adequate upgrade path. Compare migration options before deciding; tests alone do not restore platform support. It's also right when the system is genuinely small, where "legacy" does more work than the actual risk warrants.
What doesn't qualify: code that's merely ugly, a stack the team dislikes, or a rewrite pitched because starting clean sounds better than reading someone else's decisions. Those are aesthetic objections in technical clothing — most of the requests I see.
Pitching stabilize-first to a client who wants "rebuild it properly"
Clients ask for a rewrite because "rebuild it properly" sounds like progress and "stabilize what's there" sounds like doing nothing new. The pitch reframes the order, not the goal: stabilization makes a safe rebuild possible. A strangler-fig approach delivers it incrementally, subsystem by subsystem, without a multi-year freeze on new features.
Name the original developer's departure honestly, too. One person holding all the undocumented knowledge was already a single point of failure before you arrived. A rewrite by a new team, with no characterization net, just recreates that fragility under a different name. Backups, monitoring, and characterization tests aren't the slow path — they're what makes every step afterward fast, letting you finally tell whether a change made things better or worse.
Related Posts
Need help applying this approach?
Legacy System Improvement & Modernization
Review a live system, identify the most urgent problems, then improve it in stages with data migration and handover planning.
Explore related servicesDescribe the operational problem you are facing so we can discuss a suitable next step.