Legacy code is likely to evoke a lot of negative emotions, especially when we take over an application without documentation and have to figure out what is going on inside. We can spend a lot of time investigating nested dependencies or debugging old bugs. But can we make it easier and friendlier?

We can try to simplify this process by preparing a few steps that we can repeat each time we find ambiguities.

After years of maintenance and delivery of new features, even the best-written application can become legacy, and for many reasons. When I start working with legacy code, I am not afraid or disgusted. I try to find joy in improving the code, and I try to make it cleaner and prettier in small steps.

This post presents a day-to-day view of the actions we undertake when working on Legacy system modernization projects with the focus on JavaScript frontend applications, but the process can be reapplied to other areas as well, such as backend development or in-house developed products.

Magic touch from ESLint and Prettier

What could be easier than automatically applying our rules to a project? 

My first step is always to verify that the Prettier and ESLint rules have been configured. If so, I try to customize them for the team already working on the code. If not, then I have my own set of rules ready and can configure it in a few minutes. 

This is a small step, but of great importance to me. If the code is not written consistently, if the indentation and general style of the code is different in different places, then I feel lost.

ESLint will be our guardian angel and can prepare us to find a lot of bugs before we run this application. The application will be more stable and predictable thanks to adding some rules.

But I do not apply these rules to all files at once. I prefer to take small actions and apply them to files that might be changed in the current branch. For example, I suggest adding checking with these tools to the github workflow during the pull request. This will ensure that no one on the team misses places where the code can be improved.

Remove, delete, clean

Once, a company I work for decided to save an application with great potential. Unfortunately, we had no documentation and not much time to retain customers. The application was taken over at the last minute.

The frontend was written using Angular. There were many lines of code. Each view had a lot of functions, templates, API calls. My first thought was that it was a really complicated application, but then with a cold head I started to examine those elements. I noticed that many of those parts were not even used. 

So we found a second, simple way to make the code more readable. We started to remove unused parts of the code while solving everyday tasks. When we had to make changes in such a file, we looked for unused elements and removed the unscrupulous ones. As a result, I removed 25k lines and added only 12k lines of code, still adding new features and bug fixes.

Code that has served its purpose needs to go. Make a decent funeral out of it, and do not invoke the dark arts of Necromancy because you are not ready to part with it yet.

Rules for Young Programmers, XXVII

Duplicates

When we started working on the application, the first few weeks were crucial for the maintenance of the project. We had to address important issues requested by current customers as quickly as possible.

So you can imagine the look on my face when I had to change the simple functionality of displaying a full preview of an image, or change the styles for a common button for the entire application in over 20 files. When we started investigating the problem, we found that each of the views had a lot of elements implemented separately that were common to the entire application. And of course, in some places, the functionality of the same elements was slightly different because it was not always updated everywhere.

This was actually a good thing for us. We found a new way to easily improve the code. When the dust settled and we were no longer under pressure to deliver critical updates, instead of updating the same elements a few times, we started moving common parts into new components and making them as reusable as possible.

Sometimes it was not so easy, because seemingly similar elements that were supposed to do the same thing did a slightly different thing. And the question was whether it was intentional, or maybe the functionality had not been updated everywhere, and, finally, which functionality was correct. When in doubt, I always discuss with the client what course of action should be taken.

From JavaScript to TypeScript

I am a front-end developer, so JavaScript is a main tool for me at work. I am constantly surrounded by JavaScript code, but now I cannot imagine a good front-end application without TypeScript. With TypeScript, I can catch errors earlier and I have everything covered in my daily work.

This is another reason why my next step in working with legacy code is to convert JavaScript files to TypeScript one by one. This step is more complicated than the previous one, but can give us a powerful tool to improve the whole application and have everything covered.

When we start the conversion, we will probably encounter many issues. However, I suggest that you should not rush through the changes. I always try to convert files while making other changes. By doing it slowly, I can watch as the next pull request brings us more TypeScript files.

Upgrade dependencies

This is probably an obvious point to all of us, but I feel I should just mention how important it is to try to update all dependencies as much as possible.

Working with the latest libraries can give us the tools we need to transform legacy code. With high probability, we will also be able to implement new functionalities in an easier way. Even some bugs and problems can be caused by old dependencies, and upgrading can be the fastest solution to our defects.

Small actions

I am sure that working with legacy code and transforming it is a long-term process. I always try to do it in small steps. I try to improve something during standard bug fixes or when implementing new features.

For customers, it is not so important that the code is legacy. It is more relevant that the application works as expected and that we are able to deliver new features. So when I do these things that are really crucial to the customer, I always try to put in a little extra effort to improve a small part of our application. This is a slow way to bring the code up to date, but it is a trade-off for the needs of the customer and the developer.

In many situations, it’s beneficial to spend 10–20% of your time on work that improves the quality of the code. That’s also better for clients, compared to another approach where not caring about the codebase results in a huge unexpected cost of implementing a feature a few months or years down the line.

Is it a nightmare to work with legacy code?

I think many of us have fond dreams of building applications from scratch. Creating an application with its own structure, where we can choose the latest technologies and solutions. It is really great when we are in this situation and can decide how to implement everything step by step. 

But life also brings us to work with applications with legacy code. Is that a nightmare? 

Not for me. I try to find satisfaction in those small actions that can bring us something more beautiful. It is a truly rewarding feeling when I remember how lost I was when I first took over application development. How I tried to find the reason for strange bugs. And now I can see how the application looks and how I can provide new solutions faster and better. It is a fulfilling moment when I can already say that the application is written in accordance with best practices.

So, what’s next?

To summarize:

  1. Use automated tools, such as ESLint/Prettier, which can do the job for you.
  2. Look for occurrences of unused code and remove them from the codebase.
  3. Look for duplicates — replacing them with single implementations/components might improve project velocity in many ways.
  4. Use typed languages, such as TypeScript, or type annotations, to find errors at compile time (compared to production, where such an error might remain undiscovered for years).
  5. Find a moment to upgrade dependencies of your project.
  6. Prefer small steps over larger refactoring projects.

If you need a helping hand in managing software complexity…

Try us!

Adrian Ciołkiewicz, a devoted Frontend Developer at Makimo, demonstrates leadership not only within the team but also through his thought-provoking articles on LinkedIn and the company blog. With a fearless commitment to clarity, his topics range from Frontend technologies to software testing, always emphasizing the value of clean coding. Beyond the tech realm, Adrian is a lifelong learner, ready to expand his knowledge outside of his professional sphere. An ardent gamer, he appreciates both the thrill of video games and the strategic charm of board games. Fitness-conscious Adrian also makes time for sport, finding it an essential part of his well-rounded lifestyle.