Working with Legacy Code

Do you work with legacy code? You might ask, what legacy code is. It is a good question with many different answers:

-          Code which is using outdated languages, technologies, libraries, APIs, e.g. COBOL programs;

-          Old code

-          Code which was written by another team or developer

-          Code which has no documented requirements

-          Code not covered with automated tests

-          Messy code

-          Any code (“code becomes legacy as soon as it is written”)

So the definition is a little bit vague. Usually legacy code has few of these properties: old code, written by developers who no longer work for the company, without documented requirements, without tests, built on top of an old technology platform.

Code becomes legacy very quickly naturally, and it takes much effort from engineers to prevent it: update technology stack, write tests, keep code, tests and documentation in sync; generally, paying tech debt in time. Unfortunately, not many do it, and half a century of software engineering leads to the world where most of the code is legacy.

But why legacy code is so bad thing? Primarily, because it makes code changes very expensive. Old technology stack means that hiring a new engineer is hard, and engineers are more expensive. Messy code means that engineers should spend more time to understand the code and make appropriate changes. Code change in one place can easily break something in other parts of the program. Without tests it is hard to find these bugs: QA team will have to make regression testing manually, it can take days, and raises costs of development significantly. No big changes, because everyone is afraid to touch the code. Development is slow, no frequent releases, time to market is high.

What can be done to improve the code?

-          Just rewrite everything from scratch

It is tempting to kill the code and rewrite from scratch, using shiny new technologies, covered with tests, with good design. But it can take huge amount of time, probably years. It would be hard to get resources for such a project.

Meanwhile you continue to support current legacy system, make changes there. Each change you should make twice.

Such rewriting is a good opportunity to review the requirements, and remove old stuff which is no longer relevant to your business goals. Downside is that you probably do not know about old useful undocumented requirements. For some applications you can make beta testing to get feedback about missing functionality. For customer facing applications it might be harder to find missing requirements: you only see that metrics like conversion or retention rate gets worse but do not know why.

You will also come up with technical problems which were already solved in previous system, but got forgotten. For example some external API you use throws errors on some inputs and you should build a workaround. Or some library you use for data access caches stored procedure signature internally, and you should reset the cache when signature changes to prevent errors.

-          Rewrite module by module

It can be harder technically if you have monolith application: instead of writing good code from scratch you will have to refactor existing messy code to be able to break it into modules. And then, when an interface for a module is defined, you can rewrite the module from scratch using the same interface. It looks like multiple iterations of previous scenario: you will review requirements, rebuild the module, find that something is missing, and fix the bugs.

But it is certainly less risky, and easier to troubleshoot. And probably cheaper, because fewer changes will need to be done twice (in old and in new code).

-          Gradually improve code

In this case you always have one system. You just constantly improve the code: break into modules, cover with tests, refactor, remove unused code, etc.

It is less risky. Each change is very small, you can test it, you can measure business impact of each change and it is easy to isolate issues. But you will carry burden of all the old, possibly not relevant, requirements, unless you put some effort to analyze business impact of various features of the product and remove useless ones.

What is your experience? Which ways of improving legacy code did you use, and what were the consequences?