A common phenomenon among developers is the desire to start all over again. Let's throw away all the legacy code and start fresh. This time everything will be better, we tell ourselves.
The best-case scenario is that we might avoid repeating the old mistakes but make new ones instead. However, the worst and much more likely outcome is that we create new problems and repeat many of the errors that led us to throw away our old code in the first place.
When we think about the fictional new and perfect code, we fail to consider all the nasty edge cases the old code could handle. We don't think of the many times that looming deadlines led to particular trade-offs. Let alone the fact that most people working on the new codebase have different opinions about how good code even looks.
How can we do better?
First, start with an analysis of the old code and make a list of concrete problems. What is it that makes this code so hard to work with/buggy/slow?
Second, try to find solutions to each of those problems. You might find that some of those solutions are pretty straightforward, quick fixes. Others might be massive undertakings. In the latter case, try to break them down and find ways to update your codebase incrementally.
If you think it takes too much time, always remember that what you're competing with is a complete rewrite of your entire codebase. When fixing the actual problems one by one, we can integrate those refactorings into our regular sprints. Every time we implement a new feature, which touches one of the areas where we have problems, we fix those problems around the code we need to touch anyway. Use the Branch by Abstraction strategy to make it possible to refactor parts of your code one by one.
The process is something like this:
Write or improve tests for the old code.
Refactor the code.
Add tests for the new feature.
Write code for the new feature.
Sometimes Starting from Scratch Is Unavoidable
Suppose our application relies on an old framework that doesn't get any security updates anymore. Or we inherited a complete and utter mess, and none of the original developers are there anymore who could help us make sense of it. In that cases, a complete rewrite might be the best option.
Still, we should start by analyzing all the good things and all the bad things. It can also make sense to write tests for the old code if we know that our new system should have the same features as the old one. That way, it is easier for us to make small changes to the old code while we're still working on the new one, and then we already have the tests in place that we need to write for the new project anyway.
With that kind of preparation, we set ourselves up for success by preventing us from repeating the old mistakes and neglecting what worked well in the old codebase.