How to Rescue Software You're Afraid to Touch
Every developer eventually inherits it: software that works, ships to real users, makes real money — and that nobody wants to touch. Not because it's bad, exactly. Because it's opaque. Eight years of decisions nobody wrote down, encoded in a codebase where every file knows about every other file. You can read it. You cannot safely change it. And to someone new, those are the same thing.
That's the real definition of unmaintainable: not ugly, not old, but unchangeable without fear. The fix you need is three lines. The problem is you have no way to know what else those three lines will break, so you don't ship them. The feature slips to next sprint, then to never. The code calcifies, not because it's frozen by design, but because everyone's too scared to move it.
I spent a long time on an app exactly like that. Here's how it became maintainable again — and why the thing that made it possible now wasn't possible a couple of years ago.
Why legacy stays legacy
The way out of unmaintainable code has been known for decades: build a safety net of tests first, then reshape the code underneath it. This isn't a secret. Every senior developer knows it.
So why doesn't it happen? Because the net was always too expensive to build after the fact.
Retro-fitting tests onto a god object is the most thankless work in software. You write thousands of lines, ship zero features, and spend weeks reverse-engineering behavior nobody documented — just to end up exactly where you started, except now there are tests. Leadership hates funding it. Developers hate doing it. And end-to-end tests, the most valuable kind for exactly this situation, were historically the worst offender: brittle, slow, a mountain of plumbing — mock backends, fixtures, fault injection, UI drivers — before a single assertion runs.
So the net never got built. And without the net, nobody dared refactor. That's the loop that keeps legacy code legacy. It was never a knowledge problem. It was an economics problem.
The economics just changed.
The net is cheap now
The single biggest thing AI changed about rescuing old software isn't that it writes features faster. It's that it collapsed the cost of the boring, unglamorous scaffolding that made rescue uneconomical in the first place.
That end-to-end setup that used to eat weeks — a standalone mock backend that replays recorded responses per scenario, injects faults on command, fakes auth, serves binary assets; UI flows that log in, book, hit an auth gate, and assert what the user sees — is now an afternoon. Not because the concepts got easier, but because the typing got free. The plumbing was always the tax. AI pays the tax.
So the first move in a rescue, the one everyone skipped because it was too expensive, is finally cheap enough to actually do:
Freeze the visible behavior before you understand it. Whatever the app does today — working or broken — becomes the specification. Not what it should do. What it does. You pin the happy path, the empty states, the expired session, the payment flow, exactly as they behave right now, with a net that doesn't care how the code underneath is organized. That net will survive everything you're about to do to the internals, which is the entire reason it's worth building.
Tests are how AI checks its own work
Here's the part that matters most, and it's easy to miss.
A test suite isn't just protection for you. It's the thing that lets AI safely do the rest of the rescue — because it gives the model a way to verify itself.
Hand an AI a 900-line god object and say "make this maintainable," and you get exactly what you'd expect: confident, plausible, well-formatted output that may or may not preserve what the code actually did. It looks right. It compiles. You have no idea if it still books a car. You're now reviewing a large diff with nothing but your own judgment as the check — and as I argued in I Was Wrong About Five-Star Developers, your judgment is the real ceiling on what AI can safely produce. That ceiling doesn't scale to hundreds of refactors. Something else has to do the checking.
Now do the same thing with a green test suite in place. The loop closes. The AI changes the code, runs the tests, sees red, and corrects itself — before you ever look at it. The suite is the oracle. It converts the model from generating plausible code into converging on correct code, because now there's a ground truth outside its own confidence. Without a net, AI refactors at the speed of your review. With one, it refactors at the speed of the test run, and you review outcomes instead of line-by-line guesses.
This is the real unlock. AI writes the net cheaply, and then the net is what makes AI trustworthy enough to tear down and rebuild the code behind it. The two halves feed each other. Neither works alone: a net with no leverage is just slow, and leverage with no net is just fast damage.
Then modularize, in changes small enough to be boring
Only once behavior is pinned do you start pulling the pile apart — and the unit of work shrinks to almost nothing.
No more "general rework." Every change is now the smallest one that stands alone: extract one controller out of the god object; decouple one view model from the framework so it's testable in isolation; take the screen with thirty-eight parameters and group them into a single state object; collapse two duplicated error stacks into one. Each is its own branch, its own pull request, its own green run. Hundreds of them, deliberately dull.
Small changes against a green suite have almost nowhere to hide a regression, so the tests genuinely protect you and the AI both. And something quieter happens too: the fear drains out. Not through courage — through repetition. The first structural change is terrifying. The fiftieth is Tuesday. You deploy small, you deploy often, nothing explodes, and the muscle memory of dread gets overwritten by a track record.
Where to actually start
You don't boil the ocean. You don't pause the roadmap for a quarter to "add tests." That framing is exactly why it never gets approved.
You start with the next thing you have to touch anyway. The bug you're scared to fix, the feature wedged into the god object. Build the net around that flow — cheap now — pin its behavior, then make your change against a suite that shouts if you broke something. Ship it. Next time you're back in that area, the net's already there, and you extend it. The coverage grows along the paths you actually work, which are the paths that actually matter. The rescue funds itself, one boring PR at a time.
The methodology here is decades old. Characterization tests, then refactor: nothing new. What's new is that the expensive half — the net, especially end-to-end — went from too costly to justify to an afternoon, and that same net turns AI from a plausible-garbage generator into something that checks its own work before you do.
The order is still the whole trick. Net first, surgery second. Freeze what is, not what should be. Small enough to be boring. Let confidence compound. The goal was never clean code — clean code is a side effect. The goal was to make changing your own software feel like nothing again. For a long time that was too expensive to be worth it. It isn't anymore.