Working on legacy projects is not an easy and most enjoyable task, that’s why most developers would rather work on a greenfield project, than struggling with a few-years old codebase with a lot of legacy, lack of tests and deprecated solutions. Legacy usually brings us many challenges, difficulties, and dilemmas that we usually don’t have to think of when working on greenfield projects. Fortunately, years of experience in dealing with legacy have taught me how to address them effectively and enjoy making things right. In this article, you will find some useful tips and hints on this topic.
What is a legacy project?
In software development, a legacy project is usually described as a project that’s old and hard to maintain. The issues are not only connected with outdated codebases but also the tools and scripts, the infrastructure, or unsupported operating systems, hardware, and formats.
Why is maintaining technical debt important for your business?
Although such a process is costly in terms of developers’ time and company’s financial resources, the benefits of structuring the code can be disproportionately higher in the long term. It’s an important investment that will pay off in the future. It’s similar to any other debts – a financial debt may allow you to grow and scale your business faster, but if the debt is too large, it can slow down or even ruin your business. So having technological debt is not necessarily a bad thing but it’s important to be aware how “high” it is and how much it’s going to cost you in the long term. Examples of how technical debt may impact business in the bad way are not being able to add new features in a reasonable time, struggling with stability of the system (and thus possibly losing clients), high costs of maintaining the infrastructure because some parts of the code are not optimized, utilize most of the resources and scale poorly.
Main challenges of legacy projects
The main characteristics of the legacy systems are:
- no project documentation
- lack of tests
- high complexity of the codebase
- coupled, not testable code, classes with thousands lines of code, methods that just do too much and no one really understands
- old technologies, design, and architecture practices
- lack of communication with the original owners of the code
- no tracking of technical debt
Let’s be honest, every code at the stage of pushing it to production is already a bit of a legacy due to the high frequency of new updates in the project’s dependencies, adding new features that affect previous ones, changing old features to meet business needs and so on. That is why an agile approach is so important here so that the reaction to dynamic changes can be as quick as possible.
How to deal with them effectively?
1. Review the original documentation and requirements
This step may seem time-consuming for you but in fact, it will save you more time in the long term. Reviewing the documentation with the original requirements will help you in understanding where the code came from and make changes to it later on. Having all the necessary information will also prevent you from making any modifications to the code that would cause undesirable actions.
2. Test the code
Without conducting tests of the code, you’re not able to know all of its capabilities. Testing your code reveals more possible scenarios of how your code should actually work and gives you a better understanding of possible flaws and edge cases of the functionality you intend to implement. Unfortunately, legacy projects are often characterized by the fact that the code used in them is insufficiently tested, and changing it usually requires introducing a regression somewhere.
I have two messages for you – a good and a bad one. The bad news is that it’s good to start with reviewing tests suits and code coverage to make sure you are safe with changing the code without breaking things and keep the most important functionalities running. If there are no tests at all, it’s good to start with setting up a testing environment, select the right tools and write some proper tests. The good news is that you can make it a little easier for yourself. According to some tips included in Working Effectively with Legacy Code written by Michael Feathers, you should try some of these methods:
- go through the software as if you were a user, taking notes about what it does, step by step, and trying to understand why it works the way it does
- to help yourself prioritize your testing, focus on functionality first rather than the appearance
- learn the application to discover its critical areas and understand how different modules fit together; it will also help you to catch the key functionalities and focus on them in the first place
3. Divide the code into a modular monolith
In a broader perspective, the solution to problems with legacy projects is to gradually break down a large system (monolith) into individual, less coupled modules and layers within the same application (modular monolith) or into microservices if it’s really neccessary. The division of the code within a modular monolith gives us the following benefits:
- It’s easier to maintain documentation for a single module than for a large application (it’s worth using documentation autogenerators, which derive information about how the documentation should look like directly from properly prepared tags in the application code). This largely eliminates discrepancies between the documentation and the application code.
- Changes made in modules that are not highly coupled don’t extend beyond them to other parts of the application. This prevents unexpected errors appearing in completely different parts of a large project and results in less conflicts when merging changes.
- Small code packages are easier to be covered with tests.
- Modules are easier to maintain because of the way of introducing updates – updating a smaller number of files and dependencies in a module is much easier than a larger number of files in a big application. What’s more, thanks to more frequent updates, the technological debt doesn’t arise.
4. Refactor the code or start from scratch?
When working with legacy code, you’ll probably struggle with refactoring some really poorly implemented parts that you can’t really understand. In such cases it might be actually better to start from the very beginning instead of trying to understand and improve already existing code – in some cases it will save you both time and nerves. Even when it takes you more time to create new code, you may come up with a much better solution than you would end up with by refactoring it, which will ultimately also be beneficial. Just remember to not introduce many new approaches and patterns – remember it’s still easier to maintain the code that follows certain patterns used in the project than experimenting with several different approaches in different parts of the application, just because you are used to something or just because, in this particular case, it would be nice to try out a completely new approach.
5. Keep the code clean
To make things easier for yourself and others in the future, make sure that the new code you write is clean. This will help avoid problematic situations in the future or improve bug fixes. You are not responsible for the code you inherited, but you can ensure the quality of the new code you write by keeping it clean.
6. Ask more experienced developers for help
When working on re-engineering legacy software, it’s good to get help from more experienced experts who know the codebase better than you do. This will speed up your work and prevent yourself from wasting your time guessing.
Do you want to learn more how to deal with the legacy project challenges?
Don’t get discouraged with the legacy!
Although working with legacy projects is demanding, challenging and usually it is not as attractive as a greenfield project with the latest tech stack but in the end it may also give a lot of satisfaction, new knowledge and it’s not as scary as many developers imagine. It’s good to stick with good practices and rules and listen to the wise advice of colleagues more experienced than us, and above all – not to give up!