Taking Over a Legacy System: Stabilize First, Rewrite Never (Almost)

Taking Over a Legacy System: Stabilize First, Rewrite Never (Almost)
A client calls with a system 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. Every one of those instincts is wrong at this stage. 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, point it at real data, and watch it boot.
This surfaces the first honest inventory of the engagement: which environment variables are load-bearing, and which cron jobs nobody mentioned. It also turns up which API keys are hardcoded into an old config file. None of that shows up by reading code, only by running the thing and watching what breaks.
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 that has never been restored is not a backup; it's a hope. - 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." Once a system is in production, it is its own specification, whether anyone likes what it does or not.
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 system stays sellable the whole time it's being rebuilt.
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 is right when the platform is unsupportable — a runtime with no security patches, a framework with no maintainers. No amount of testing changes that. 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
Building something similar?
Hotel Management System Development
Custom ERP-style hotel management software: bookings, room status, invoicing, staff, and WhatsApp automation — built around how your hotel actually runs.
See how I can help