We live in a world where progress is king - complete tasks, meet goals, commit more, deploy frequently, ship faster. But what happens when something is lurking in the codebase that’s holding you back? That’s right, legacy code. Let’s take a look at how you can deal with legacy code, once and for all.
This guide will cover the following topics👇
- What is legacy code?
- The characteristics of legacy code
- The problem of legacy code and technical debt
- Tips and tools for working effectively with legacy code
- Legacy code migration and cultural shift
- Resources for dealing with legacy code
When we talk about legacy code, your first thought might be of old, obsolete code. Yes, it may be written for operating systems that are no longer supported. However, it may also mean code which is:
- Inherited code: it may be from another developer, team, or older software version.
- Code that the original developer no longer maintains.
- A code base that is no longer engineered but continually patched.
- Legacy code can also refer to unsupported operating systems, hardware and formats.
Ionis characterises legacy code as “the antithesis of clean code which is easy to understand, maintain, and adapt. Legacy code is unwieldy, outdated, and messy, which can lead to numerous problems.” It’s typically resistant to bug finding through automated tests.
Nicolas Carlo calls legacy code code you’re not comfortable changing but also “valuable code you’re afraid to change” and “the code you need to change, and you struggle to understand.” He suggests that you may, in fact, be responsible for “that code you wrote because you can’t remember the hell you had in mind when you did. Yes, our past self often makes silly mistakes. (If you ever need a good reason for great documentation, this is it).
Sometimes legacy code exists for a reason. It may point to a team with a crazy high turnover, making little effort in their documentation. Or a team of more experienced developers who are comfortable with older languages. A company may keep legacy code due to competitive advantages over other businesses. They may fear that rewriting code may introduce new bugs or remove hidden functionality.
However, legacy code may also operate in a black box that is less accessible to a broader development community, meaning insufficient quality improvement, security, and maintainability. When people leave, it may be hard to find new developers appropriately skilled to work with it. As time goes on, the code may struggle to work with new and emerging apps and hardware. The company risks falling behind and is drowning in technical debt. Do you hate legacy code?
Ivo Lukač did a survey which revealed over half of the respondents would rather not work on it, and an additional 11% hate it.
Specifically, their pain points are many, with spaghetti code and fear of breaking something the most common concerns followed by the frustration of sticking to depreciated technology. This survey offers an insight into the complexities of dealing with legacy code and explains some of the reasons developers prioritise other work.
Intrinsic to legacy code is technical debt - each time the code changes hands, it needs more and more features and bug fixing, all of which is time-consuming and frustrating, and the amount of technical debt continues to grow.
When working with legacy code, time and energy is precious and you need to know where to concentrate your efforts. Planning and purpose are critical - you need to know what can be changed and what to ignore. Fortunately, there’s plenty of tools available to make life easier:
Static code analysis is the process of debugging by examining source code before a program is run. Code is analyzed against a set (or multiple sets) of coding rules. Then you get a diagnosis report of any violations of these rules based on their severity. This helps you prioritise the most urgent or important tasks.
In general, static code analysis is a good practice even beyond legacy code. It creates the opportunity to identify and eliminate code defects before code is pushed for functional QA, saving a lot of later pain and reducing technical debt. Some examples of static code tools:
A lot of IDEs that are used for code refactoring already use static analysis.
Code refactoring is one way to deal with legacy code. It’s about overhauling code to make it more understandable, maintainable and adaptable through optimizing code, simplifying and merging variables and classes and rewriting command methods.
As Martin Fowler, author of two books on refactoring, explains:
”Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure. It is a disciplined way to clean up code that minimizes the chances of introducing bugs. In essence, when you refactor you are improving the design of the code after it has been written.”
However, you can only begin refactoring once it is clear how the code works - and doesn’t work. The code is then optimized piece by piece. Any unnecessary code is removed or rewritten. Classes and variables in the code are simplified and merged. Command methods are adapted and rewritten. In the end, refactoring is basically a general overhaul of the legacy code. The resulting code is easier to understand, maintain, and adapt.
Developers should test code after refactoring to check for any breaks. Continuous integration is useful as it enables you to revert to a previous build if something goes wrong.
Stepsize VS Code and JetBrains extensions help Engineering teams gain actionable insights into their technical debt, including those caused by legacy code. Engineers can track debt, add bookmarks, and organise their TODOs in their code editor, plus see the impact of technical debt and prioritise it in the webapp.
There’s increased interest in migrating legacy systems to low code development platforms. Sylvain Leroy wrote a succinct but in-depth guide to legacy software migration a few years back that’s worth a read. There are also practices like migrating desktop or on-premise data centres to the cloud and the shift from monoliths to microservices. While discussing all of these in great detail is beyond the scope of this article, it’s important to explore these trends in regard to cultural change and company culture:
- How do developers collaborate?
- How is knowledge passed down from developer to developer?
- What efforts are made to retain knowledge when someone leaves?
- What are the company standards for documentation and who ensures they are met?
- How does the company prioritise time spent dealing with technical debt? What incentives do they offer (especially important if everyone hates doing it: maybe they need to provide more time off or bring in specialists?
- How do you ensure that the new code is clean and error-free from here on? What about training and development?
• 4 Techniques to Work with Legacy Codebases—A talk by Nicolas Carlo at the Managing Technical Debt Meetup.
• Refactoring Java, Part 1: Driving agile development with test-driven development—Kata-based series by Oracle on refactoring basics to help set up a test-driven development environment.
• Working Effectively with Legacy Code by Michael Feathers.
• Approaches to refactoring, technical debt and legacy code— Tim Wise has put together a great list of resources.