by Isaac Palka
"Tech debt" has become such a ubiquitous, yet commonly misunderstood term. In most circles it's treated like a four-letter word (or two four-letter words to be precise). People avoid uttering it out loud, as if it will jinx their software. Everybody likes to pretend that they're tech debt-free, yet everyone has it. A lot more than you'd imagine. I'll even wager that the software you rely on day-to-day runs on mountains of tech debt. If you Google the cost of tech debt, you'll find staggering numbers on the percentage of time engineers spend on it, and its multi-trillion dollar impact on GDP.
Despite how much has been written about it, I have not yet read about practical ways to address it. Ergo, this blog post. I try to introduce ideas that you may not have considered before, such as how to quantify the ROI of tech debt, who should implement it when it's unavoidable, and how to undo it more easily. Whether you're a junior engineer, a senior engineer, a product manager, or other, I hope you find some useful takeaways.
This is my take on it:
Tech debt is the cost introduced from a suboptimal design decision that prioritizes seemingly short term gains and faster time to market over long-term sustainability.
If you have no idea what I'm talking about, it's possible you've been involved with tech debt before and didn’t even know it. If you’ve debated “short-term vs. long-term” or “tactical vs. strategic” trade-offs with engineers, you were probably discussing tech debt. If you heard that work will need to get undone or redone, you were probably discussing tech debt. If you’ve managed to piss off engineers during a design discussion, it’s likely you pressured them into creating tech debt (it's a huge pet peeve among engineers). There are other tech debt "smells", but you get the idea.
It's important to clarify that tech debt IS NOT the result of building some version of the software today that won't be able to do everything you want in the future. That's just building software incrementally, which is how all software should get built.
I often hear engineers push back on work requests for reasons such as, "if we build it this way, it won't be able to do XYZ next year, and that will be tech debt!", or "this design choice won't scale to infinity, therefore it's tech debt!"
My response to that is, "that's what all software development is about!" Knowing how to deliver something today and how to evolve it over time is the essence of good software engineering. And an engineer who can't build software incrementally and actually ship something is not worth a penny. The key is how to build something today such that next year it won't be a nightmare to add a new feature, or to scale it further.
Imagine you need to get across a river. You and your friends construct a bridge made out of tree branches and rope in a few hours. It's hardly a robust bridge, but it does the job you need right now. This hack bridge may last for a long time. But is it ready to transport tens of thousands of humans, and heavy vehicles? Of course not. Is this tech debt? Nope. There was no conscious trade-off of quality vs. time to market, and there weren't expectations for this bridge to support heavier loads in the future.
Now, pretend you're constructing a massive steel bridge for commercial use. A year before the expected completion, the client suddenly demands that you open the bridge next month. This is an impossible feat, and you explain that one of the two decks hasn't even started getting constructed. Some wise guy then suggests to open up only the completed deck, and to construct a new temporary ramp that will lead to that deck. While the first deck is opened and being used, you'll finish construction on the second deck.
However, you soon realize that the temporary ramp interferes with where the main ramp was supposed to go, and due to it's hasty design, it's causing daily traffic jams. You now struggle to construct the main ramp, because the temporary ramp is in the way. You continue to come up with workarounds to support both ramps, which ultimately causes you more delays, more expenses, and a far from perfect bridge. You ultimately decide you need to close the first deck and demolish the temporary ramp, so you can properly complete construction on the bridge. Because of the extra work and complications, your project is now behind schedule and over budget.
In short, you implemented a poor design in order to achieve seemingly faster results. But the work to undo it and to redo it resulted in a net increase in time, cost, and complexity. This is the essence of tech debt.
As alluded to before, tech debt has real financial consequences. It's not very different from financial debt. Many view tech debt as something that only idealistic engineers bitch about, but which isn't that important in reality. So if you're the type who only cares about the bottom line, and can't be bothered with good designs, or engineering happiness - I assure you that tech debt translates directly into a real financial cost.
Hopefully I have your full attention now. Here are a few ways that tech debt manifests into cost:
All of these issues compound each other too, leading to the notorious death spiral. More maintenance -> lower productivity -> lower morale -> more incentive to keep adding tech debt instead of fixing it -> even lower morale -> employee turnover -> more new hires who don't understand your system -> more tech debt -> rinse and repeat. Eventually, this can lead to an entire product or company collapsing. I've seen first-hand how a team can fail to deliver one feature in an entire year because of the mounting tech debt.
Finally, the section you've all been waiting for. As per the title of this article, this would all be moot if I don't offer some practical advice. I split this up into three sections:
Despite the obnoxious / cocky tone of my writing, you must remember to never approach tech debt with judgement or ego. For starters, you genuinely don't know the context and history of the product you're working on. Tech debt is rarely implemented with bad intent. Rather, it's usually due to lack of understanding or awareness.
When you start addressing tech debt issues, be like Dale Carnegie - don't condemn, criticize, or complain. This is the golden rule. If you don't follow it, the rest of the points here are moot. If your first point of order is to call out how bad a code base is, nobody will listen to you or want to work with you. So try to be more like a doctor - diagnose the situation, and offer cures, in a non-judgmental way.
This may sound facetious, but I'm dead serious. Start adopting the mindset of not agreeing to tech debt, and start building software properly. Would a doctor cut corners because a patient demanded that a procedure be done faster? Hopefully not any doctor I'd go to. Would a chef serve undercooked food because the customer was yelling that he was hungry NOW? No. Then why should an engineer cut corners just because someone who doesn't know any better is insisting that you need to deliver things faster? The pressure may also come from your own engineering peers, who should know better. It's your duty to educate and influence others to take the right approach (which in my experience, will usually also be the fastest approach).
Just say no to tech debt (without getting yourself fired), and start saying yes to quality, modular, extensible code. Don't let others suggest to you how to perform proper engineering, especially if they're not engineers. Don't even fall into the trap of entertaining the idea and getting into long discussions about it. This doesn't mean you'll always successfully avoid it, but it sets the right tone with your colleagues and business partners.
You may be thinking "that's not advice, we have to in order to please stakeholders and get products shipped". Believe me, you won't please any customers with rushed, half-assed software. And while you may please your boss today, you won't be for long. But mostly, it boils down to ethics, and your identity as an engineer. Some engineers are artists. Others are code monkeys. The choice is yours.
Being aware you have a problem and discussing it is already a step in the right direction, even if your advice ends up falling on deaf ears. It's possible that your peers are not even aware of the situation or of the consequences. So bring it to others' attention. Present options, evaluate trade-offs, and explain your concerns to your peers and managers. Communication alone goes a long way.
Compile a list of questions around tech debt, and incorporate them into a template that you use for tech specs / feature requests. When new work comes up, you'll naturally have a forum where you can discuss tech debt before any software work begins. It'll become a routine part of planning and engineering design, which makes things much easier.
Some sample questions:
I also recommend to formalize a sign-off process, and have engineering and non-engineering stakeholders acknowledge the following in writing (or via an email or JIRA ticket):
A senior engineer should know how to implement tech the right way and the fast way. It's often possible, but you need the right person. They will evaluate all aspects (speed, scale, security, time to rebuild) and make a sound judgement. A senior engineer will often be able to get you the shortcut you need today whilst keeping the door open for the longer term work. Again, it is sometimes necessary to do a "hack job", but a strong engineer will implement it in a way that it will be trivial to undo.
A senior engineer will find this tip obvious, but not everyone reading this is a senior. Tech debt is often the result of improper architecture, or messy code. However, if you wrap your crap in the interface you'll plan to have in the future, most of your problems go away.
For example, you may need to display data from a new source in your app. You don't have time to design a full schema, deploy a database, wrap it in an API, and have your front-end call it. I've seen hack jobs where engineers will have the front-end load a CSV file directly. Before you know it, this monstrosity grows, and you find you have 5 apps loading an entire collection of CSVs.
If you had built a light-weight API that behind the scenes loaded the CSVs, at least your front-end could have called a simple API. Later, when you had to undo this mess, at least the front-end wouldn't have to change, and the tech debt would have been isolated to what's behind the API. Same goes for any interface, whether it's a function call, a class, etc. Limit the blast radius of your bad designs by wrapping it in a clean interface. This small extra effort is going to buy you a ton of mileage, and can be a good balance between shipping code faster, and not horribly ruining your code base.
I said it before, and I'll repeat it here - knowing how to build modularly and evolve the code is the essence of good software engineering. And a trick to doing that is to always think about the interface you're exposing, even if behind the interface is a big piece of crap.
It's not always easy to understand your own code after enough time has passed. It's harder to understand someone else's code, even when it's good. It's nearly impossible to decipher bad code, especially without the context of why the decisions were made. So be a good samaritan, and document the context, the decision, and the tech debt implementation. Litter your code with notes about where you'll need to undo or redo things. Not just for others, but for yourself. I guarantee you won't remember all the details when you revisit this code in a few weeks.
Now that I shared some ideas on how to avoid tech debt, let's discuss how to address existing tech debt. It may be your own, or it may have existed before you joined the project you're on.
Most of the tips from the previous section still apply - talk about it, evaluate trade-offs, get others to commit time and resources, etc. And definitely don't continue the cycle of mounting more debt upon the existing debt.
The biggest hurdle I've seen to addressing tech debt is getting the green light to invest the time and money into it. Business stakeholders may want more features, and not have an interest in (or understanding of) the importance of tech debt. Maybe even your engineering peers are not inclined to take on the challenge. It could be out of fear, lack of motivation, or lack of understanding (e.g. I used to work at a financial firm that hadn't upgraded its Fortran code because "it works and it's risky to touch it". P.S. It didn't f*ing work). So in addition to tech tips below, there are psychological and political tips.
This is similar to the prior tip of "wrap your crap" in the interface you want, but in reverse. Martin Fowler coined this term as an analogy to rewriting software. I won't do it justice by describing it in detail here, so feel free to Google it. The general idea is you wrap existing tech debt behind the proper interface, and then you have the freedom to change the messy code under the hood layer by layer without downstream consumers being affected (similar to how this plant wraps itself around the branches of an existing tree, and gradually works its way down, strangles the existing tree, and eventually replaces it completely).
When you are at the planning table, propose your ideas to address tech debt. Get specific about what you'll do, and why it's important. Quantify the ROI you expect. If you can frame the work from a customer or revenue perspective, you'll increase your odds of getting stakeholder agreement.
For example, don't request to "fix API tech debt". It sounds vague and pointless. Rather, pitch that "we can decrease page load time by 500ms by refactoring API code, which studies have shown can increase customer sales by 3%". The work you'll do is the same, but if you know your audience and speak their language (i.e. the language of $$$), you'll get more cooperation.
It's not always obvious what the ROI is, and perhaps there isn't an immediate benefit. But as an engineer, you just know the code needs work, otherwise disaster may hit in the future. So highlight the negative ROI - what is the potential cost or business loss if the tech debt isn't addressed. You can also include the probability of such an event happening. Again, try to quantify it and tie it to customer value / revenue.
For example, don't say "if we don't address tech debt in our API, it won't scale!". Rather, say "in the past 4 months, we had 8 incidents where the server crashed due to a high load, which caused 40 unhappy customers emails, 5 dropped customers, and a loss of $6,000 in revenue. If we invest 5 days into refactoring this code, we'll eliminate these types of issues. If we don't address it, and extrapolate these numbers, this can continue happening 24 times per year with a potential loss of $18,000" (or more, because as we grow our business, this will happen more frequently).
Keep a log of tech debt items, framed in a business context. Whenever you're up for a discussion on planning work, or prioritization, those items will be front and center to the decision makers. Make sure to point out which future features in your company's roadmap will not be doable until that piece of tech debt is addressed. It's also helpful to see the amount of tech debt items overall, rather than randomly point out individual items as they come up. Seeing the totality of it can help raise awareness.
Pad your estimates, and as you address ongoing work, try to tackle some tech debt along the way. Document the undocumented code. Refactor the duplicate code. Automate manual steps. It's kind of like cleaning your house - you can do a lot of little things that over time amount to a lot. And you can at least leave the code you worked on in a slightly better state than when you found it.
If there are 1-2 takeaways you should leave here with, they should be (1) to treat tech debt like financial debt, with financial consequences, and (2) to stop caving to the pressure of implementing poor code to meet deadlines. If you were considering taking on financial debt (e.g. mortgage, student loan), you'd probably consider the following:
Treat tech debt with a similar mindset, and you'll see better results.
© 2023 IsaacPalka.com. All Rights Reserved.