There is a sentence I have said, and heard, in almost every company I worked for. You open an old part of the system, the code is ugly, the logic is tangled, and out comes the line: “we should just rewrite this from scratch.” It feels obvious. It feels brave. It is, most of the time, the most expensive bet in software, and the one with the worst odds.
Joel Spolsky wrote about this back in 2000, in Things You Should Never Do, and called the full rewrite the single worst strategic mistake a software company can make. Twenty four years later I have collected enough scars to agree with him, and to add a few of my own.
Why the rewrite is so tempting
The pull is emotional, not technical. New code feels clean. Old code feels like someone else’s guilt. We are builders, and a blank file is a fresh start with no embarrassment in it. Reading old code is harder than writing new code, so the old system always looks worse than it is, simply because we have to struggle to understand it.
So the story we tell ourselves is seductive. This time we know better. This time it will be clean. This time will be different.
It is almost never different.
What the ugly code actually is
Here is the part the rewrite fantasy ignores. That weird condition you want to delete is not random ugliness. It is a scar. Somebody hit a real bug, with a real customer, and that strange line is the fix. The check that makes no sense is a payment edge case from three years ago. The comment that says “do not remove this” is a tombstone for a production incident.
The ugly code is full of knowledge nobody wrote down anywhere else. When you delete it to start fresh, you are not deleting mess. You are deleting years of expensive lessons, and you will pay to learn most of them again, one production surprise at a time.
The math nobody puts on the slide
A full rewrite has a cost structure that quietly destroys budgets.
While you rewrite, the business mostly freezes. The new system is not ready, so it ships nothing. Meanwhile the old system still runs, still breaks, still needs maintenance, so you are now paying two teams, one to keep the past alive and one to build the future. And the future always takes longer than promised, because you rediscover every old edge case the hard way. The new system is two weeks away, for eighteen months.
During that whole time your competitors keep shipping features while you ship a flat line. That is not a technical cost. That is a strategic one, and it is the kind that does not show up until it is too late to undo.
When a rewrite is actually honest
I am not saying never, I am saying prove it. A rewrite can be the right call, but only when you can show the reasons with more than frustration. The platform is genuinely dead, the language or runtime is end of life with no security updates. The cost of change in the old system is not just annoying but measurable, and rising. The team can run the new and the old side by side, so the business never goes dark. If you cannot make that case with numbers, you do not have a rewrite decision, you have a rewrite mood.
And even then, the smartest path is rarely a big bang. Martin Fowler describes the strangler fig, where you grow the new system around the old one and replace it piece by piece, until one day the old trunk is gone and nobody had to hold their breath. I used a version of this myself once, keeping a shell around a legacy system and loading new pieces from outside it. Boring, gradual, and it let us sleep at night.
What I do instead
When the urge to rewrite shows up, I treat it as a signal, not a plan. Usually it means one part of the system is genuinely painful to change. So I work in small steps, in this order.
First I check if that area has test coverage. If it does not, I write tests before touching anything, because tests are the net that makes every later step safe. They also force me to understand what the old code actually promises, which is half the battle. Then I refactor the part where the work is actually happening, and I put a clean boundary around the worst area so the rest of the system stops depending on its guts. When I am ready to swap a piece, I hide the change behind a feature flag, so I can roll it out slowly and turn it off in seconds if something breaks. Only then, if it still makes sense, I replace that one piece behind the boundary. Small steps, each one shippable, each one reversible.
The new tools help here too. I now use AI to read and explain the scariest legacy code, the stuff whose author left years ago. It is not magic, and it gets things wrong, but a fast first explanation of a thousand line mystery function is a real gift when you are deciding what to keep. It makes understanding the old code cheaper, which tilts the math even further away from throwing it out.
The honest version
The rewrite is so attractive because it sells a feeling. A clean start, no inherited shame, code you are proud of. But the company does not pay you for pride. It pays you to keep delivering value while the system stays alive and changeable.
So before you bulldoze, prove it. Put the cost of the rewrite next to the cost of refactoring in place, with real numbers, including the frozen roadmap and the second team and the rediscovered bugs. If the rewrite still wins after an honest count, do it, carefully, in pieces. If it does not, you just saved your company a very expensive lesson, and yourself a very long eighteen months.
Old code that works is not your enemy. It is a paid education sitting in your repository. Read it before you burn it.
Pax et bonum.