How Code Becomes Legacy
Your current software running in production might be considered legacy today, but it has managed to achieve something remarkable: it solved the problems it was created for.
As technologies evolve it gets harder and harder for software to survive the test of time. With exponential progress, software tends to start feeling outdated much faster than it used to. Analysing the main factors impacting the longevity of our code and how to improve on it will be crucial to ensure our solutions will live long and prosper.
Old Technology != Broken Software
Software built with old technologies is commonly considered legacy. It does not necessarily mean that the code itself is completely broken, though. The usage of outdated technologies does tend to impact the long-term maintainability of a project in different ways. It might cause serious hiring issues due to the inability of finding developers familiar with these technologies, or willing to work with them. The solution might also need a complete rethink due to the ubiquitous changes happening in tech all the time.
Nowadays things are really spiralling out of control. There seems to be a new language, library or framework every day that will be able to solve all of our problems. Frontend technologies are clearly one extreme, with applications built only last year now being considered legacy and in need of a replacement.
One of the easiest tricks to ensure your software will live longer is avoiding to write code tightly coupled to libraries and frameworks. Reducing the number of dependencies of your codebase is always a no-brainer. Making sure the majority of the code you write does not rely on a gazillion third-party libraries should be a priority.
Developers tend to love new shiny libraries and frameworks and don’t think twice about writing all of their code around them without considering that newer libraries have an intrinsic higher risk of being abandoned. At the very least newer libraries will come out with new versions that might require big rewrites due to the high pace of change in immature technologies.
Try to put an effort in keeping libraries and frameworks away from the code that provides value for your company. Keep in mind that languages tend to stick around much longer than libraries and frameworks and use it to your advantage. Java is one example of a language that has managed to keep up quite well so far, even though alternatives are popping up everywhere.
Not knowing how the current system is currently supposed to work is another reason why developers tend to label software as legacy. If the original authors are not around anymore and documentation is lacking it can be really hard for newcomers to make changes to the codebase. This usually impacts new feature release and increases the chances of continuously breaking some parts of the system.
In the ideal world, you would want to maintain a comprehensive documentation and the knowledge about the current system by spreading it to more than just single individuals. Easier said than done.
Lack of automation and tests
The inability to automate builds and deployments has a huge impact and will increase the friction of trying to keep your code and infrastructure up to date. In addition, the lack of automated tests might not directly make a codebase obsolete, but it will definitely make it harder to change. A codebase harder to change leads to lack of confidence causing dependencies not to be updated when necessary or code not to be refactored due to the unknown impact of the changes.
High coupling and lack of modularity
Most of the legacy software I’m familiar with has very high coupling between components. High coupling results in a codebase that is extremely hard to change causing the same issues outlined above.
Make modularity a first-class citizen. A single module will be much easier to change than a cross-cutting change across a complex system. Building a modular codebase allows to isolate future changes, or allow to redesign and rewrite part of the system without impacting others.
Lack of care
As I wrote about previously, developers need to care. One of the most common reasons why software becomes legacy is due to the fact that it has not been looked after by someone that truly cares about it. Libraries haven’t been updated. Tests haven’t been written. Developers moved on to new projects working with different technologies. Not caring leads to software rot. Authors walking away from their code is one of the biggest threats to the lifetime of your software, especially when it has not been carefully planned.
Another extremely dangerous threat is a broken design. A broken design will leak everywhere, will make it really hard to change things without breaking everything. It will require an endless amount of workarounds. At some point, you might want to stop and plan on fixing the root cause of the issues. Re-designing and re-architecting a broken domain model, or a broken process can be tough, but it can greatly increase how long your software will be able to exist in production without requiring a complete rewrite.
One of the reasons why software can end up with a broken design is usually due to the number of assumptions the developers made when writing the code. Assumptions should be validated, always.
What is the expected lifetime of your software?
Consider asking the expected lifetime of the software solution you are going to work on. Sometimes businesses just want to try things out, and would not mind some throw-away code. In other cases, businesses are in for the very long term and want to build solid solutions. Make sure you ask the question in order to not be disappointed if your efforts will go down the drain in a few months, or if your MVP and all your assumptions are now expected to run in production forever.
Allow your code to evolve
Your complex systems should be built for change, built to evolve. I make the ability to change one of the highest priorities in the code I write every day. Requirements are never set in stone, and they are never 100% reliable. Assumptions, even if validated, might end up being wrong. You might ask the wrong person, so trust no one. Try to insure yourself for the future and make sure that if anything does go wrong, you will be able to fall back to a plan B.
One of the most important thing that allows you to evolve your software will be how you handle your data. One of the main reasons why I personally love event sourcing is how decoupled the data is from the actual code relying on it. It is also designed to keep all of your data around rather than doing destructive operations on it, which can be handy when new requirements come along.
It might look like I am promoting extreme future-proofing. As always, common sense should be applied, otherwise you might end up with heavily over-engineered solutions. Providing the wrong abstractions will make your code harder to change instead of making it easier.
Manage your urge to rewrite
I cannot stress this enough, but you should stop rewriting your projects. You should learn to distinguish working code that does not require immediate action from what is threatening the maintainability of your system. If technologies are becoming obsolete, you should plan how to nuke them out of existence.
I see developers jumping onto new frameworks and rewrites every year. The fact that you hate the code you wrote a year ago doesn’t mean you’re going to love what you are going to build next. Work on improving your coding skills instead, not the technologies you are using. If you’ve managed to build a messy codebase with a simple MVC framework, you’ll probably make an even bigger mess switching to a reactive one. The same applies with monoliths and microservices. More modern technologies rarely mean the same developers will end up writing better code.
Plan an upgrade path
Instead of jumping into a full rewrite by default, try to plan an upgrade path first. Simple things are easy to rewrite from scratch, but for most complex applications it just isn’t feasible. If you have millions of lines of code in your codebase there is no way your teams will be able to rewrite them in one go without putting the business on pause for years. And you’ll probably end up with a different kind of legacy anyway, or at least different kind of problems, that’s guaranteed.
Another thing to keep in mind is that a full rewrite will require you to have a very solid knowledge of what the current system is doing right now, and that is rarely the case in complex systems.
It’s important to remember that when you start from scratch there is absolutely no reason to believe that you are going to do a better job than you did the first time. First of all, you probably don’t even have the same programming team that worked on version one, so you don’t actually have “more experience”. You’re just going to make most of the old mistakes again, and introduce some new problems that weren’t in the original version. — April 6, 2000 — Joel Spolsky
Respect your legacy
I struggle, at times, to justify calling a codebase “Legacy”*. A codebase should be considered legacy only if it has been built with now obsolete technologies or if it’s using now obsolete processes. Anything else is just the *“Current Working Software” running in production. You can’t call legacy something you’ve built a couple of years back either, that just means you haven’t done a really good job at maintaining and evolving it.
Having code you wrote more than a decade ago still running in production is a great achievement and definitely something to be proud of. If you currently have projects that no one really knows how they work or no one really wants to work on take a step back and try to figure out what went wrong. Understanding the reasons behind it is critical in order to avoid making the same costly mistakes again. An incentive might need to be in place in order to find someone with the required skills or willing to take care of what is now widely considered your legacy code. It might end up costing a lot of time and money, but it will bring valuable lessons. Next time, make extending the lifetime of your code a priority.