When we speak about legacy projects, it’s common to reach a point where there’s so much technical debt that you can’t implement new features anymore.
The code was hacked around repeatedly. Now it’s unmaintainable. And you’ve reached the point of no return. You need a solution to move on.
You may have tried to rework parts of the app, but every refactoring pulls the rest of the app, because of its inter-twinned nature. So you give up on the rework.
Maybe you tried to write unit tests instead, before you rework any part, but that code wasn’t designed to be testable in the first place!
Finally you decide, maybe you should just freeze the app and stop touching it anymore. And rewrite a new one from scratch,
In this article, I will discuss why rewriting a legacy project from scratch may not be a good idea for you. I will also discuss an alternative approach to rewriting from scratch.
Why rewriting from scratch doesn’t work
Changes to code can be dangerous, and refactoring can be costly.
In this scenario, rewriting it appears to be a decent idea. Starting from the ground up.
Here’s how it normally goes:
- You and management debate a strategy for deferring new features for a while, while you revamp the existing software.
- To cover what the existing software does, you estimate the rewrite will take 6 months.
- A serious bug is identified a few months in, and it must be corrected in the previous code as well. As a result, you patch both the old and new code.
- A new feature was sold to the client a few months later. It must be implemented in the old code because the new version is not yet ready! So you return to the old code, but include a TODO to implement this in the new version.
- You understand the project will be late after 5 months. The old app did far more things than you predicted. You begin to work harder.
- You begin testing the new version after 7 months. QA brings up a lot of issues that need addressing.
- The company can no longer stand “not developing features” after 9 months – they agreed to 6 months. Leadership is dissatisfied with the situation, and you are exhausted. While trying to keep up with the rewrite, you start making changes to the old, terrible code.
- Eventually, the two systems are deployed to production. The long-term plan is to replace the old one, although the new one is not yet ready. Every feature requires two implementations.
Does this ring a bell? Don’t feel bad about it; it’s a common blunder.
So, what exactly are the problems here
- With so much back and forth between the old and new projects, it may take you a long time to complete the new project and phase out the old one. Or maybe even never.
- You have ended up deploying two systems to production, now new features will cost twice the time to implement.
- Perhaps the fun part is since the old app wasn’t built to support the new features you want and the new app is too out-of-date (because it lacks many features that the old app has), it won’t be long before a team member suggest that you should instead rewrite the old app properly. Follow that route again and you can be sure that you will have 3 systems running in parallel in a few months.
Don’t worry. We’re not going that route. Because there is a more efficient way to work around legacy systems.
How to rewrite a legacy codebase efficiently
The technique is straightforward:
Progressively delete the old code base, in favour of a new one.
This is known as the “The Ship of Theseus pattern”, in reference to this old thought experiment on the notion of identity.
The ship wherein Theseus and the youth of Athens returned from Crete had thirty oars, and was preserved by the Athenians down even to the time of Demetrius Phalereus, for they took away the old planks as they decayed, putting in new and stronger timber in their places, insomuch that this ship became a standing example among the philosophers, for the logical question of things that grow; one side holding that the ship remained the same, and the other contending that it was not the same.
— Plutarch, Theseus
Will the ship remain the same when all of the ship sections have been replaced as they rot?
Or more specifically, can your users know if you’re gradually replacing your codebase?
Of course, the idea is to escape the trap of an endless rewriting. Instead, you’ll take an incremental step-by-step method.
Here’s the plan:
- Allow the new code to serve as a proxy for the old. Your user use the new system in theory, but actually, it simply redirects them to the old one.
- Re-implement each behaviour in the new codebase while keeping the end-user experience unchanged.
- Allow users to consume the new behaviour while you gradually phase out the old code. Delete any code that is no longer in use.
What’s the advantage?
- The best part is that you solve the problem of delivering new features while rewriting.
- You are not duplicating the code with this method. And you don’t have to add new features twice!
- You also get the new system into production as soon as possible. You get feedback faster, which means less work and a lower risk of breaking something.
- Finally, you can gradually implement the rewrite. There is no need to freeze code for 6 months.