In this article
- Why is a full rewrite usually the wrong call?
- What is the “mess” in a legacy codebase actually made of?
- What is technical debt, and does it mean you need to rewrite?
- When is a rewrite the right call?
- How do we decide when we inherit a codebase?
- What does legacy system modernization look like in practice?
- How do you start?
When we inherit a codebase, the answer is almost always to refactor and to replace the riskiest parts in steps, not to rewrite the whole thing. A full rewrite is justified only when the platform can no longer be run safely, or the core design can’t do what the business now needs. Even then, the safer route is to get there one piece at a time while the system keeps running.
The rewrite vs refactor question can be answered in about a week, by reading the code and running it before anyone commits to a plan. This is how we decide, and the reasoning behind it.
Why is a full rewrite usually the wrong call?
A full rewrite throws away years of fixes and stops your product from changing while it’s underway. Joel Spolsky called rewriting from scratch “the single worst strategic mistake that any software company can make” (Joel on Software, 2000 (opens in a new tab)). His example was Netscape, whose last major release was almost three years old by the time its rewrite reached a first public beta.
His reasoning still holds. The code looks like a mess because it’s harder to read code than to write it, and the odd-looking parts are usually bug fixes that took weeks of real-world use to find. A rewrite discards all of that and starts finding the same bugs again.
Martin Fowler, writing 24 years later, reaches the same place from the modernization side: a plan to build a new system that does exactly what the old one does has gone “down in flames most of the time.” Replacing a serious system takes a long time, users can’t wait for new features meanwhile, and the details of what the old system really does are hard to pin down (Strangler Fig, 2024 (opens in a new tab)). The same logic applies to a smaller system. The old one still needs fixes while the new one is built, and the roadmap stalls.
What is the “mess” in a legacy codebase actually made of?
It’s usually one of three kinds of problem, and each has a different fix. Spolsky’s breakdown is the most useful one we know, and we add two more that we see often.
Only the last row ever points toward replacing something, and even there it’s a component, not the whole system.
What is technical debt, and does it mean you need to rewrite?
Technical debt is the extra effort that poor internal quality adds to every change. Martin Fowler describes it as a metaphor coined by Ward Cunningham: the extra effort it takes to add new features is “the interest paid on the debt” (Technical Debt, 2019 (opens in a new tab)). His example is a feature that would take four days in a clean module structure and takes six in a confusing one. The two days are the interest.
The useful part of the metaphor is what it says about paying it down. Fowler’s advice is usually to pay the principal off gradually, spending a little extra on cleanup each time you touch an area. Interest is only paid when you change the code, so crufty but stable areas can be left alone, while areas that change constantly need a zero-tolerance attitude. Technical debt is a reason to clean the right places in the right order. It isn’t, by itself, a reason to start over.
When is a rewrite the right call?
A rewrite is the right call when the problem is in the foundation, not the code on top of it. In our experience that means one of two situations:
- The platform can’t be kept safe.The language, framework or hosting is past the point where it receives security fixes, and the code is so tied to it that it can’t be upgraded in steps.
- The design can’t meet the requirement.The issue isn’t that the system is slow in places, it’s that its structure rules out what the business now needs, and no amount of tidying will change that.
The second is easy to underrate, so here is a case from our own work where the limit was in the design. Oversightis a monitoring dashboard we built at Afiniti. Operations teams had to watch distributed systems producing huge volumes of telemetry, and a single-node setup couldn’t keep up, so there was lag between an incident and seeing it. That was a structural limit, not a tidiness problem. Oversight was designed for throughput from the start: events are processed asynchronously, current state sits in a Redis cache, and the work is spread across load-balanced Node.js instances. Six systems now appear on one dashboard, under a second behind.
Notice what is not on that list: messy code, old code, code nobody on the current team wrote, or a missing test suite. Those are reasons to refactor, and they are very fixable.
How do we decide when we inherit a codebase?
We read it, run it and measure it before we recommend anything. Five checks, in this order:
- Build and run it. Can we get it running on a clean machine, and how long does that take? If nobody can, that is the first thing to fix.
- Check the safety net. Which parts have tests, what is monitored, and how do problems reach the team today? Systems that fail silently need tests and alerts before anything else.
- Find where change is slow. Look at the recent features that took far longer than they should have, and the files everyone is afraid to touch. That is where the interest on the debt is being paid.
- Look at the data. Messy data is a project of its own, and it follows you into any new version. When we moved MarkazFlow’s client off paper ledgers, we cleaned and validated years of records against the paper logs before writing the new system. More than 10,000 accounts moved with no data lost.
- Ask what the business needs next year. A system that is awkward but does what you need is a different case from one that cannot do what is coming.
The result is a written diagnosis and a priority list: what to fix first, what can wait, and whether any component really needs replacing. That is what the first week of a fix and scale engagement produces, and it is yours to keep either way.
What does legacy system modernization look like in practice?
Done safely, legacy system modernization is a series of small, shippable changes: tests around the risky parts first, then one component at a time replaced or restructured while the system keeps running. Fowler calls the gradual version a strangler fig, after the vine that slowly replaces its host tree. You find the seams in the system, build a replacement for one small piece, move its behavior across, and repeat. Each step carries little risk, and the business gets value early instead of at the end.
The first step is making the system safe to change, and the best example we have of what that looks like is Afiniti Mega AI, the servers behind an enterprise AI product, built at Afiniti as part of its engineering team. Every new feature shipped with its unit tests and API tests, so coverage stayed above 90% as the product grew. The platform ran at 99.99% uptime through continuous feature growth, and most production bugs were fixed the same day. That is the state we try to leave an inherited system in: one where change is routine, because a mistake is caught before release instead of after.
In practice the order is: add tests and monitoring around the parts that hurt most, fix the causes of the worst incidents, refactor the areas that change often, and replace a component only when the foundation check above says it has to go.
How do you start?
Start by finding out which of the five rows in the table you’re in. If you’re weighing a rewrite, ask whoever proposes it to show you the evidence from the first three checks: what was run, what was measured, and which part of the system actually can’t be saved.
If you’d like a second opinion, the discovery sprint for this kind of work is one week of reading the code, logs and incident history, ending in a written diagnosis you can act on with us or without us. Start from the contact page, or read how we work on the fix and scale page. If the real problem is hosting or the cloud bill rather than the code, see our guide to reducing AWS costs.
Sources
Keep reading
- How to build an MVP: the steps, the timeline and what drives the cost
How to build an MVP in six steps, what to include and what can wait, how long it takes, what drives the cost, and the mistakes that lead to a rewrite.
- How to cut your AWS bill without slowing anything down
Nine practical ways to reduce AWS costs, in the order we work through them: finding waste, right-sizing, storage, data transfer, then Savings Plans and Spot.