At this point, technical debt is so meaningless, yet so powerful, that it has become part boogeyman, part scapegoat. It’s the reason we’re scared to change our code. It’s the reason we can’t deliver that next feature on time. It’s to blame for our low morale. And it’s a reason to resent our colleagues on the business side.
We all know the story: If only our managers wouldn’t pressure us to deliver the next feature, we could do it right. But we never get the chance. We tell them there’s too much technical debt, but they don’t stop pushing us for new features.
But like all vague terms, scapegoats, and boogeymen, it hides the complexity of reality.
I’ve heard the term technical debt used for so many different things that they’ve become a blur. It’s messy code, lousy architecture, unfinished database migrations, unused files that haven’t been deleted, refactorings on the backlog, impedance matches between abstractions, things you’d like to do but never get around to, and even features that were promised but are now past their deadlines. And when I ask complaining programmers what makes their work so miserable, the most common answer is “technical debt.” I’ve come to hate hearing the term technical debt and I want us to stop using it.
Where did it come from?
The original idea behind technical debt is that we can temporarily trade speed now for speed later. That is, we can rush now, but we’ll be slower in the future. Programmers understand this intuitively through experience. We know that we can cut corners, make guesses, and ship sloppy code to get the feature to customers. And we know that comes at a cost. As the corners, unlucky guesses, and slop pile up, we’ll need more cognitive work to keep everything from breaking.
So Ward Cunningham came up with a metaphor: Moving fast now is like taking out a loan. You get cash now (the principal), but the loan will cut into your profits in interest payments. And if you don’t make interest payments, the principle will get bigger, making the interest payments even worse. It’s a clever and elegant way to help a business person understand the tradeoff.
Technical debt is a good metaphor in most ways. There are two damning problems with it.
We cannot measure technical debt, nor can we measure programmer productivity to understand the slowdown.
Financial debt has unlimited capacity, while programmers have limited cognitive capacity.
Let’s talk about both.
Technical debt is not a lineary quantity
In finance, debt is a mathematically known quantity. You can generate amortisation schedules to know exactly when debt will be paid off. You can calculate interest payments and the consequences of not paying interest. It’s all numerical like we expect money to be. Further, the way debt is doled out is also rational. The higher the risk for a loan, the higher the interest rate. Finally, interest payments are additive. If you have two loans, one with a $10k per month payment and one with a $20k per month payment, the total payment is $30k. However, technical debt is not measurable, it doesn’t behave rationally, and the interest is not additive.
How much technical debt do you have? How long will it take you to “pay off”? How much is it slowing you down? You don’t know. You can’t know. There’s no way to decide if you have too much, or whether you can prudently take on more. Or how long you should spend to pay it down. In short, you can’t treat it like a finance expert does.
Also, not all technical debt is equal. You could save weeks with a crazy hack, and never slow down in the future. Or, you could name one measly function wrong, saving seconds, but costing your team days of productivity every time they read the code. Technical debt interest rates do not follow any rational order.
Finally, technical debt slowdown is not additive. A hack that slows you down one hour per week, combined with another hack that slows you down two hours per week might slow you down 10 hours per week, because the number of corner cases are multiplicative. Unlike in finance, we don’t have nice additive interest. We have a non-linear, multi-dimensional design space where every decision affects many others.
Cognitive capacity limits how much debt we can handle
In finance, you can pudently take out more debt as long as your revenue is growing faster than the interest payments. Your new revenue will easily pay for the added interest. However, as Janelle Arty Starr explains, programmer cognitive capacity is limited and doesn’t scale linearly with the number of programmers. If our code is complex enough, we can’t hold it in our heads. It takes exponentially more time to understand it because we have to swap it in in chunks. If your code passes a single person’s cognitive capacity, the time it takes to modify it goes superlinear.
And adding new people doesn’t scale linearly either. This is where I seriously disagree with this article. You can’t just hire to keep up with interest payments. Complex systems require more time to learn, so your onboarding will take longer. Coordination costs go up superlinearly. In my opinion, your best bet is to take the time you would hire and onboard and reduce system complexity so your current team can handle it. Next best is to find a nice module barrier and split teams in two, then grow each to no more than 5 or 6 programmers.
Debt can’t take all the blame
I want to take a brief pause before I get to the alternatives to talk about another issue that’s not really about the metaphor, but it’s super relevant. We blame everything on technical debt, when sometimes software just gets complicated and you have no means of paying it down.
For example, if you break your monolith into microservices, you’ve got the added complexity of distributed systems problems. Can you call the extra complexity debt? Would you ever pay it down? I would not put it in the category of debt, but it does significantly impact your speed of delivery and the cognitive capacity required to maintain your software.
Further, features have a carrying cost. In addition to the time it takes to develop new features, interest payments on your technical debt, you also have to pay for the upkeep of your current code. It’s not free. Don’t blame debt for that cost as your number of features increases.
So I guess the metaphor, besides being bad, is also overused.
Alternatives to technical debt
My preferred remedy to the term technical debt is to stop using it as a suitcase word and start being specific. Don’t call messy code techdebt. Call it messy code. Don’t call a wrong guess techdebt. Call it a wrong guess. Specificity can help. Don’t bucket them all together.
If you complain about technical debt, your colleague might agree: “Yeah, the debt is so bad!” But you don’t get anywhere because there’s no communication. But if you’re more specific, “We’re moving slowly because that module is a mess of leaky abstractions,” maybe they’ll disagree: “Naw, it’s not the abstractions, the architecture needs to be redone.” You may not agree, but at least you’re communicating.
I also like the idea of calling it pain. This metaphor doesn’t have the baggage of the finance world and it humanizes it. “It was painful to understand that module when I had to modify it.” If you will have to modify it again, it’s worth reducing that pain before next time. Specificity and quantification go a long way, here. How long were you unable to make progress? And what exactly might have helped at that time? This is the main idea in Idea Flow by Janelle Arty Starr (and this talk by her about the same topics).
Starr also chooses to talk about increased risk. Finance can give a kind of security to increasing debt when revenue is increasing. But risk always sounds bad to business people. “Doing X now will make it risky to deliver Y.” Maybe we should think of modules as risky. Think about schedule risk, budget risk, and stability risk.
What if we just called it complexity? Would that nudge us to simplify it? Unfortunately, one person’s complex hack is another’s obvious solution. You need good design skill to see how to simplify it.
Could we just call it the carrying cost? That is, once code exists, it has a cost to maintain, modify, and even just to be there taking up space. If it’s messy, its carrying cost is higher. And we can reduce the cost by cleaning it up.
And a final idea, which is much less developed but I think will prove to be rich, is to go deeper into the metaphor. Debt, in the finance world, is leverage. You can do more today because you get a big lump of cash and pay for it in the future. So we should think about leverage, not financial debt. In code terms, leverage is how much functionality you get from a marginal increase in complexity. A small increase in complexity may be justified if it increases the functionality significantly. What if we said “this code isn’t giving us enough leverage?” Or even, “I have an idea to get more leverage from this module.” I hope to develop this idea soon.
Merry Christmas!
At work, our approach is to create a specific Jira ticket identifying a change we acknowledge "should" be made in the future as a result of a decision we are making today. That way, anything we consider "technical debt" is visible in the backlog. Sometimes those tickets are created by the developer as they take a conscious shortcut, sometimes they are created as a result of pull request reviews, sometimes they just come out of team discussions. The key point is that all such acknowledgements are actionable and treated as "future work" along with everything else.
We may, at some future date, review such tickets and decide "not worth doing" but that still makes us consider the tradeoff we made more closely and we are then making a conscious decision that "this technical debt is acceptable and we're prepared to just write it off" rather than "pay it down".
Great article with good insights. I'm surprised how I can relate to this article. It is almost as we were working for the same company.