First, let’s define what technical debt and legacy code are — both terms commonly used in the software industry. They actually speak about a similar thing, but from two angles. Technical debt is an estimated cost of future development to make the code optimally maintainable again. It mounts over time driven by shortsighted and suboptimal choices. Technical debt does not apply just to code, but to the overall system and its architecture. It behaves like a financial debt — it piles up overtime. If software is usually counted as an asset, technical debt is the liability side. On the other hand, legacy code is literally the code used for a particular software which has substantial technical debt.
For example, replacing one developer with another creates debt. The new developer inherits the (legacy) code and usually dreams of completely rewriting it :). Even when the same developers work on the same code, they get more and more into trouble, as they spend more time on adding new features and fixing bugs. Hence, their work gets more expensive — the debt grows.
How does it get to that point? Easy. There are basically two main ways:
- The debt grows inherently even if you do nothing. All code is more or less dependent on some other external code, could be libraries, frameworks, system services, etc.. Usually, external code evolves and influences your code in some way. This is usually called “bit rot”.
- The debt grows every time when the code is changed but not in a sustainable way. Hack here, hack there, super important new feature glued without much thinking, temporary function added then forgotten, and so on.
It just happens. You are under some deadline and you have no time to do it properly. Maybe you implemented the solution which was too complex in the first place. There are not enough tests or you just didn’t have the skills or experience to do it right or you just delay cleanups and upgrades.
It is unavoidable. There will always be situations where you create some technical debt. The big difference is:
- Is it a conscious decision and that the payment is planned?
- Or there is no awareness, nor acknowledgement of its existence?
Ideally, if you understand the situation, you will, the sooner the better, take time to make the code optimally maintainable again. It will cost in the short run, but less in the long run.
The danger occurs when the debt is not repaid. It grows and eventually you can get yourself in a very unpleasant situation where you get stuck with no options left.
So, how you manage technical debt is very important for your long-term projects. If you have some short-lived project that you are 100% sure you will not change the code then you don’t need to worry about the technical debt. Otherwise, you need to pay it one way or another.
Paying the debt
As you well know, paying financial debt earlier is always cheaper. The same applies for technical debt. In general, paying the technical debt is done by refactoring the code - making the code easier to maintain. Sounds reasonable but there are two main challenges to overcome:
- The first one is that refactoring takes time, hence there is a cost (of paying the debt). Project or product owners, clients and other stakeholders are usually reluctant to spend money on things they can’t see. Frequent refactoring usually doesn’t produce any visible change as changes are under the hood. This needs to be budgeted in advance and calculated into the TCO (Total Cost of Ownership), otherwise, it is hard to justify it later and ad-hoc (dilbert). Successful long-living projects have a defined percentage of the developers time to be allocated for dealing with technical debt, and it could be up to 10%-20%.
- The second challenge is related to the developers themselves. Average developers love to create new things, but changing and improving some legacy code is not that sexy. Even more, some developers hate it. There is no easy way to overcome this except continuously educating and training developers to write clean code and to do so on existing codebase whenever they are touching it. It is possible to turn things around and have developers brag about successful removal of thousand lines of code.
I did a simple survey at the SymfonyLive Berlin 2019 during unconference session with around 35-40 people in the room. Only 20% of people stated they rarely work on legacy code. Almost ⅔ would rather not do it or even hate it.
Here is what frustrates developers when dealing with legacy code:
To be a bit practical, here is a list of situations that can be a good opportunity to do refactoring more often and part of the regular development routine:
- when some critical code is discovered to be hard to maintain
- when there is no one that understands the code
- when fixing bugs
- when adding new features
- when making documentation
- when onboarding new developers
What does refactoring exactly include is a much bigger topic than this post and there are many excellent books and tutorials, but here are just a few simple examples:
- Cleaning code by removing dead code and implementing DRY
- Making dependencies abstract
- Improving readability by enforcing code styles and better naming
- Breaking bigger chunks of code into smaller chunks
- Replacing code gradually with strangler pattern (more here and here)
- Add tests
- Or just simply follow Boy Scout Rule (leave the code better than you found it)
These are all small changes, no need to do all at once, but they add up over time and keep the technical debt lower!
Here is what I asked the crowd in Berlin about this topic, although one answer was too tempting :)
Maximum Viable Product
Technical debt is serious business, as many projects found out at some point when they got paralysed unable to do needed changes. A clear sign of technical debt is when there is a high probability that something will go wrong when fixing a bug or adding a small feature. It is actually easy to track how much every code change costs and monitor it over time, but what is not simple is assessing the business costs of not delivering a new feature.
There are more clues on how to spot higher technical debt:
- It is hard to upgrade the project dependencies
- It is hard to scale
- There are known bugs that don’t get solved
- Regression errors happen often
- Deployments are troublesome, often fail for various reasons
- Documentation is out of sync
- Data is inconsistent
When technical debt suffocates the project, then it's time to start the old debate: refactor or rewrite (refactor-vs-rewrite, avoid-rewriting-a-legacy-system-from-scratch-by-strangling-it ).
But technical debt is not just a developer level thing. It exists also on the architecture level and product level and beyond. For example, global stats show that around 20% of software features are often used and 45% never. A good product owner would recognize this and start kicking out unnecessary features. This directly reduces technical debt as there is less code to maintain.
While at the beginning of the project there is often a Minimum Viable Product (MVP) as an important milestone, I think in the long term, software should converge into a Maximum Viable Product — consisting of used features and pruned from the features users do not use.
Managing technical debt as one of the important tasks for product management includes:
- Monitoring clues and acknowledging technical debt
- Fixing issues and bugs early, going to the root cause
- Encouraging writing clean code
- Enforcing code quality standards
- Integrating early
- Testing early
- Reserving time for refactoring
The better these tasks are incorporated into the development process — the less the technical debt will grow and easier to control it.