The True Meaning of Technical Debt 💸
"Know your enemy, know yourself, and in a hundred battles you will never be defeated"
Last week I came across this old article by Rands about managementese.
It’s a great essay on communication — and how, in the workplace, communication naturally optimizes for clarity and speed.
Weird phrases that we often mock, such as "action item", or "circling back", are actually highly efficient vectors of meaning. They fit non trivial concepts in very few words, and are understood by everybody.
One of such great metaphors is Technical Debt.
Technical Debt is widely used and discussed within engineering teams. It’s a successful concept, I believe for a couple of reasons:
It's intuitive and backed by common sense — something that piles up and makes you slower and slower, just like interest on existing debt.
Management understands it, yet it stays under control of engineering.
These elements make it a perfect leverage when negotiating priorities, deadlines and resources.
However, is it really as clear as it sounds?
It is a fact that, over time, all development teams get slowed down by the existing codebase. But why? Is it because maintenance is inevitable? Or because we could do something better in the first place? Or both?
Hey 👋 this is Luca! Welcome to a ✨ monthly free edition✨ of Refactoring.
Every week I write advice on how to become a better engineering leader, backed by my own experience, research and case studies.
You can learn more about Refactoring here.
To receive all the full articles and support Refactoring, consider subscribing 👇
❓ Why Technical Debt Happens?
If you search for "Technical Debt" on Google and look at excerpts from the first page, you will find some kind of consensus.
It looks like it is something that accumulates because of some nasty, dirty practices. If true, it should follow that by writing clean code from the start (and documentation, and tests, etc.), debt never piles up, or it does in a negligible quantity.
Of course, if you have any experience working in an engineering team, you know this is false.
Technical debt creeps in even when you work with the best intentions, and follow the very best practices.
But why?
🔍 Technical Debt as disagreement
Enter this short video by Ward Cunnigham.
Ward is the original inventor of the "Technical Debt" term, and it's revealing to hear the sentence in which this metaphor was used the first time (emphases are mine):
If we fail to make the program aligned with what we understand to be the proper way to think about our [...] objects, then we are going to continuously stumble into disagreement, and that would slow us down like paying interest on a loan.
Ward describes debt as the natural result of writing code about something we don't have a proper understanding of.
He doesn't talk of poor code — which he says accounts for a very minor share of debt.
He talks of disagreement between business needs and how the software has been written.
But how do we land to such disagreement? In my experience, there are two offenders:
🎨 Wrong Design — what we built was wrong from the start!
🏃 Rapid Evolution — we built the right thing, but the landscape changed quickly and made it obsolete.
Let's see both more in detail 👇
🎨 Wrong Design
It may happen that the team designs a solution that doesn't properly fit business requirements, in a non-obvious way.
Requirements may still be met on the surface, but something is wrong under the hood — an abstraction that doesn't fit, some duplication, scalability issues, and so on.
Of course, such things may happen because of poor development skills. But in my experience, most of the times it's because developers didn't fully understand what had to be done.
How to improve? 💡
If this happens regularly, a natural response is to invest more in the design phase. This means, of course, more engineering analysis, but also more talking with stakeholders. You should discuss both the here and now, and the future evolution of the project.
Increasing Design effort brings diminishing results after a while, so it's up to you to find the sweet spot, based on how solid is your company planning vs how much change you can expect in the future.
🏃 Rapid Evolution
The second scenario is when things change so rapidly, that today's requirements cannot really be trusted.
That is: even if we build the right thing, it risks becoming obsolete fast, because of the ever-changing business landscape.
How to improve? 💡
A thorough Design phase provides limited protection against future changes, so a leaner cycle might work better:
🏃 Rush — the code out of the door
🔍 Learn — more things about your business reality
🔨 Refactor — put that learning back into the software
Basically, we take a piece of the Design effort and move it forward, in a Refactoring phase that happens every time a few pieces have consolidated and are ready to be secured.
In this scenario, we are accepting the creation of debt, trusting our ability to repay it in the short term
This ability comes down to three elements:
Writing code that is clean to refactor — which is very different from clean. Such code is similar to those sailor knots who should be strong enough to hold for a while, but easy to dismantle when not needed anymore.
Actually learning along the way — analyze, with the help of stakeholders, what still fits and what is going to become debt.
Spending regular time on refactoring — reorganize the code periodically to reflect your updated understanding. This is easy to dismiss, letting the debt pile up until it’s too late and you need a big rewrite. Taking baby steps every time is more sustainable both for your business, and your development team.
📌 Takeaways
As always, I try to summarize the main points of the article in this final section.
This should be useful for the reader, but it is also a reality check for me to understand if what I have written makes sense from top to bottom 😅
So, if you are in a hurry, that's what you should remember:
Technical debt is caused by a lack of understanding — it stems from disagreement between business needs and how the software has been written.
You can limit such disagreement... — by spending more effort on the design phase, and by properly discussing requirements with stakeholders.
...or you can embrace it — by explicitly planning for lean releases and following rework. This is a better choice when the future is uncertain and might bring radical changes.
📚 Resources
A few resources that helped me write this article, and can be useful to dig deeper into this topic:
📺 Debt Metaphor — a short video by Ward Cunningham that explains the relationship between debt and understanding.
📃 Technical Debt as a Lack of Understanding — a recent article by Dave Rupert that reflects on Ward's definition and adds up valuable real world experience.
📊 Technical Debt Quadrant — a thoughtful essay by Martin Fowler about what Technical Debt really is, and its different types.
💬 Managementese — a great article by Rands about management speak and communication in the office.
And that's it for this week! What's your experience with technical debt? Let me know in the comments 👇 or via email!
For my Italian friends 🇮🇹 you can find here the Italian version of the article: Cos’è il debito tecnico e come affrontarlo in modo agile.
Hey, I am Luca 👋 thank you for reading this through!
Every Friday I publish some advice around product and engineering management, and how to improve your work in a team.
If you liked this post and you haven’t already, you can subscribe below and receive weekly updates in your inbox!
While this describes tech debt well, it doesn't provide common underlying reasons, and for that there are a couple more concepts that have to be taken into consideration.
1. management measurement and estimation tends to only cover the initial development phase of a piece of work, and tends not to explicitly factor the ongoing and end-of-life costs. As such those costs then tend to get baked into the costs for future work that is coupled to it.
an expedient piece of work with respect to initial delivery, if it were to have an impact on all subsequent work, it reduces the ability to deliver since it's ongoing costs need to be added to all future work.
which leads into the second aspect
2. Coupling - often the expedient (technical debt) decisions allow for the coupling of work. By reducing the coupling, the costs associated with new work tend to be self-contained and not factor in prior maintenance. As such the ability to deliver over time improves even if the initial development might be higher for the decoupled work.
eventmodeling.org goes into more detail in a similar vein, but does include a good diagram to help visualise this : https://eventmodeling.org/cost-comparison.jpg
The idea of writing code *clean to refactor* really caught my attention. It is only a small shift of perspective compare to writing *clean* code, but it means looking ahead and anticipating instead of being obsessed with the current state of your problem.
I can relate to that in data science especially, where your understanding of a problem changes as you write code and thus have new results to analyze.
But I would be curious to ask what you recommend in practice to write code *clean to refactor*? I feel that building abstractions before it is really needed is a first trap to avoid.
Also, I think that Ward's quote "If we fail to make the program aligned with what we understand to be the proper way to think about our [...] objects" can be interpreted in two different ways:
1 - not having a proper understanding of the situation
2 - having a proper understand of the situation, but failing to actually implement this understanding.
The result is the same: a code which is not a good fit giving the requirements, but the solution to solve this issue could be different in both cases.
Thanks a lot for this great content Luca, I'll keep reading it with pleasure ;)