In this article, we are going to address the terminology clutter and differentiate between the terms technical debt, legacy code and maintenance load.
This term is probably the most problematic in that it is often misunderstood and some people have deliberately stopped using it for exactly this reason.
The term was originally coined by Ward Cunningham when he was involved in the development of a software in the finance sector. To make a point to his boss, he used a financial analogy: If one tries to get features out as quickly as possible instead of specifying everything in advance, he or she may discover new things after delivery that were not known during the original development. If the software is not being adjusted so that it reflects this new knowledge, there will always be additional efforts as a result of this deviation - like interest that must be paid on a loan []. If this deviation is never addressed, i.e. if the loan is not repaid, the team will be more and more busy with additional maintenance efforts and the productivity will go down. To stay with the metaphor: If I keep accumulating new debt, at some point I won’t have any money left to spend or pay off the loans, I’ll just be busy paying the interest on the loans.
Some extensions exist for this term that do not contradict the metaphor: Martin Fowler’s definition of technical debt includes consciously or unconsciously incurring technical debt because certain technical skills are lacking in the team, or deliberately foregoing a more thorough solution in order to go live with a feature more quickly [].
So different aspects play a role in the notion of incurring debt. One can look at the aspect of the cost of repayment (interest on the loan) or the conscious decision to shift effort into the future. What ultimately always remains: Every technical debt causes an additional effort a) because it exists and the software is to be extended with it, and b) when it has to be removed from the system.
In addition, the following holds as well: Bad code, which has been written out of laziness or ignorance, is what it is, bad code. Only when code is deliberately accepted in such a way, it becomes technical debt. However, such codebases often emerge inadvertently.
Nevertheless, the metaphor only helps us if we can use it to better communicate and address the problems in software development. To some stakeholders, the term helps with clarification.
Legacy code usually denotes code that can only be maintained with disproportionately high effort, for example because it was written without tests. Other meanings of legacy code include code of an outdated system that has been developed using code principles, libraries or frameworks that are no longer considered up-to-date by the corresponding team.
Here again, the following becomes clear: For one, there is no consistent definition of what falls under legacy code and what does not. And secondly, the evaluation of what exactly comprises legacy code is also always subjective. What type of code is considered legacy code tends to vary in different teams; it is not uncommon that even within teams, no full agreement exists on what is to be considered legacy code and what is not. Hence, they often don’t manage to find a sustainable approach for dealing with the legacy code of their system.
When software engineers use one of those terms, i.e. technical debt or lefacy code, they often share the same motivation: If these parts of the system continue to incur a lot of effort, and the system is still going to be developed further in the future, how do we deal with them? How do we get our code quality under control, and preferably in a sustainable way?
In order to reach a consensus on how to deal with tech debt/legacy code, the tech debt dilemma tree can help.
Another essential point for software development teams is to deal with maintenance load.
No matter whether teams are talking about technical debt, legacy code, cruft, or anything else, at the end of the day, the key question is how to deal with this code - code that is being considered as outdated or subpar from a technical perspective or difficult to maintain.
A fundamental question to ask yourself first: Is it even feasible to ever completely get rid of such code?
When teams grapple with this question, this often leads to several realizations:
Some code may not be of top quality, but it is rooted in a module, which now rarely needs to be touched or changed.
A complete removal of old unwanted code in the entire system is not realistic.
We would write some code differently today, but the approach taken back then was okay and that is what defines the system.
We can’t avoid the fact that, from today’s perspective, we would write code differently than we did back then, but we are still not immune to writing tomorrow’s legacy code today.
So, there is no one-size-fits-all approach to which code should be modernized and how. Instead, the maintenance load in the teams has turned out to be an important signal that helps with such an assessment:
By looking at maintenance load, we try to make the development activities visible that do not directly address the development of new features. This typically includes all the work where the terms above are preferably being used, e.g. rewrites and improvements of old code, modernizations and refactorings of certain code parts, fixing bugs caused by missing tests. However, it can also be the case that the team spends most of this time for other things, for example dealing with a brittle CI/CD pipeline setup. That’s why technical debt/legacy code should be assessed w.r.t. how much it slows down the team in its everyday work.
The main purpose of such an exercise is to make the significant maintenance efforts visible. Only afterwards, possible ideas for improvements should be gathered and discussed. Details on this can be found in this article.
: [Ward Cunningham on the debt metaphor]
: [Martin Fowler: Technical Debt Quadrant]