Sooner or later, technical debt turns into a relevant topic of discussion in every software engineering team. Usually, the sooner, the better.
Tech debt (referred to as TD from here on) is a useful metaphor for the extra cost and friction created when a codebase is left in a less-than-ideal state to gain short-term advantage. It takes many forms - some more dangerous than others - and in all cases is something teams should always keep in mind and work diligently to reduce as much as possible.
To preface this, I want to say that I don't think any sufficiently large development team can completely avoid creating TD, especially not when they're operating within the framework of the modern software market, with tight deadlines, exacting clients that like to change their requirements far too often, and in more recent times - the surge of AI tools used to generate what I like to call "software slop".
Many people like to illustrate the idea of TD by comparing it with interest, since it is a metaphor that maps short-term implementation shortcuts to a future cost - additional effort (interest) required to change or extend the system later. When used wisely, it can be a tool to accelerate validated learning and deliver stuff on time; used carelessly, it becomes a crippling obstacle that grows with time.
The top-level categories TD can be split into is:
On the other hand, low-interest code can be depicted as using outdated libraries or something of that nature - it's not immediately a problem, and it's very unclear if it will ever become a problem, but is definitely something that you need to keep an eye on for vulnerabilities and possible future breaking changes that cause incompatibility issues.
The way I like to think about code is that it cannot conceivably be considered "finished" until it has a comprehensive suite of tests, as well as extensive and clear documentation.
Furthermore, TD can be reasoned about using Martin Fowler's "Technical Debt Quadrant":

Both planned and unplanned TD are dangerous, but reckless TD doubly so. The idea with planned TD is that you plan to go back and refactor the code. That means you also write it in a way that will allow said refactoring in the future. This often provides a more rigid and predictable timeline of events that your POs/PMs and other Ms will be able to stomach more easily, though I wouldn't recommend expecting them to be livid about it in any case. This is what deliberate, prudent TD is.
There's also inadvertent prudent TD, which is expressed as the result of learning by working on a project. Even the best developers can't always predict what the best design for a complex project will be. After working on it for some time, it becomes clear where things could have been done better, and this can be used as a powerful learning tool for the future.
When teams underestimate where the design payoff line is and decide to believe they don't have enough time to follow the best practices, even though they might know of those best practices, the result is deliberate reckless TD. It can also be a good indicator of lazy, careless developers, so it's always worth paying extra attention to the sources of that kind of debt.
And the last kind of TD - inadvertent reckless - is what I would imagine the most common type of TD around. Anecdotally, it feels right to me - a lot of more junior people like to dive headfirst into coding, because it's fun to do so, without first reasoning about the project requirements and framework within which they need to implement the solutions, or planning about edge cases and future issues they might bump into. And to go back to the AI tools I mentioned earlier - I think this type of debt will become even more prevalent in the near future.
To expand further on the costs of TD and how it affects teams, you can think of it like this: TD shows up as friction on every future task - it’s not ‘one big rewrite’ cost, it’s thousands of small slowdowns.
High-interest debt corrodes a team's velocity with time. The more of it accumulates, the slower each new feature gets implemented. A project with a hundred tangled inter-dependent modules will inevitably take longer to work in than a properly structured project, because changing one file breaks 15 other files for no good reason at all. A bug fix takes 10x as long to find the reason for and implement the fix for, because debugging is cluttered and confusing, etc.
There's another very crucial point about this that is often overlooked by managers until it's too late - the talent and morale impact of high-interest TD. As someone that has been tasked with maintaining a legacy repository with horrible code in it, I can confirm this from firsthand experience - coding skills tend to atrophy with time, and I've had moments where I've lost almost all passion for the craft. I've found myself having friction with my team because of the mood I'm in, knowing I need to deal with yet another obscure, hardly traceable bug in the legacy mess I've inherited from a bunch of careless contractors and clueless juniors (of whom I have admittedly been).
So how does one measure and quantify TD?
One of the first things that need to be charted down is touch frequency on pieces of a system - how often do you modify existing modules or parts of a system? The ones that get the most attention are your highest-value candidates for a refactor.
The next element in the equation is the amount of what Fowler calls "cruft" in those parts of the system - "deficiencies in internal quality that make it harder than it would ideally be to modify and extend the system further". When you chart the amount of cruft on top of your most frequently touched parts of the system, you get a sort of a heatmap with zones that are both frequently changed AND difficult to change. Those are the areas you want to focus your attention on with the highest priority.
To "sell" this to your managers, you can use a sort of interest-to-income comparison - convert the TD heatmap to a simple ratio for prioritization: interest (time lost per month) ÷ benefit (revenue or product priority). When you explain it that way, it will become clear just how important refactoring can be. At that point it's time to start planning for that refactoring create a sort of "debt register" - list the "owed items" with estimated payoff cost and expected reduction in interest. Naturally, you can't be as precise in this as you can be in finance, but rough estimates should carry your point across effectively enough, provided a reasonable person is on the other end of this conversation.
To make them feel more prepared for it, you can leverage an interest rate proxy - calculate how long a change takes in a specific area of the heatmap before and after refactoring. If you've done a good refactor, the difference should be obvious enough to make the value of refactoring clear.
There are many patterns by which you can approach clearing TD. Here are some of them:
It's important to contextualize how modern organizations deal with this kind of issue. Some of the approaches include (but are definitely not limited to):
apenwarr.ca
Tech debt (referred to as TD from here on) is a useful metaphor for the extra cost and friction created when a codebase is left in a less-than-ideal state to gain short-term advantage. It takes many forms - some more dangerous than others - and in all cases is something teams should always keep in mind and work diligently to reduce as much as possible.
To preface this, I want to say that I don't think any sufficiently large development team can completely avoid creating TD, especially not when they're operating within the framework of the modern software market, with tight deadlines, exacting clients that like to change their requirements far too often, and in more recent times - the surge of AI tools used to generate what I like to call "software slop".
Many people like to illustrate the idea of TD by comparing it with interest, since it is a metaphor that maps short-term implementation shortcuts to a future cost - additional effort (interest) required to change or extend the system later. When used wisely, it can be a tool to accelerate validated learning and deliver stuff on time; used carelessly, it becomes a crippling obstacle that grows with time.
The top-level categories TD can be split into is:
- High-interest
- Low-interest
On the other hand, low-interest code can be depicted as using outdated libraries or something of that nature - it's not immediately a problem, and it's very unclear if it will ever become a problem, but is definitely something that you need to keep an eye on for vulnerabilities and possible future breaking changes that cause incompatibility issues.
The way I like to think about code is that it cannot conceivably be considered "finished" until it has a comprehensive suite of tests, as well as extensive and clear documentation.
Furthermore, TD can be reasoned about using Martin Fowler's "Technical Debt Quadrant":
- Deliberate + Prudent: planned shortcuts with a repayment plan.
- Deliberate + Reckless: knowingly rushed work without a plan (dangerous).
- Inadvertent + Prudent: design choices that later reveal better approaches (learning).
- Inadvertent + Reckless: accidental cruft from lack of understanding or poor practices.

Both planned and unplanned TD are dangerous, but reckless TD doubly so. The idea with planned TD is that you plan to go back and refactor the code. That means you also write it in a way that will allow said refactoring in the future. This often provides a more rigid and predictable timeline of events that your POs/PMs and other Ms will be able to stomach more easily, though I wouldn't recommend expecting them to be livid about it in any case. This is what deliberate, prudent TD is.
There's also inadvertent prudent TD, which is expressed as the result of learning by working on a project. Even the best developers can't always predict what the best design for a complex project will be. After working on it for some time, it becomes clear where things could have been done better, and this can be used as a powerful learning tool for the future.
When teams underestimate where the design payoff line is and decide to believe they don't have enough time to follow the best practices, even though they might know of those best practices, the result is deliberate reckless TD. It can also be a good indicator of lazy, careless developers, so it's always worth paying extra attention to the sources of that kind of debt.
And the last kind of TD - inadvertent reckless - is what I would imagine the most common type of TD around. Anecdotally, it feels right to me - a lot of more junior people like to dive headfirst into coding, because it's fun to do so, without first reasoning about the project requirements and framework within which they need to implement the solutions, or planning about edge cases and future issues they might bump into. And to go back to the AI tools I mentioned earlier - I think this type of debt will become even more prevalent in the near future.
To expand further on the costs of TD and how it affects teams, you can think of it like this: TD shows up as friction on every future task - it’s not ‘one big rewrite’ cost, it’s thousands of small slowdowns.
High-interest debt corrodes a team's velocity with time. The more of it accumulates, the slower each new feature gets implemented. A project with a hundred tangled inter-dependent modules will inevitably take longer to work in than a properly structured project, because changing one file breaks 15 other files for no good reason at all. A bug fix takes 10x as long to find the reason for and implement the fix for, because debugging is cluttered and confusing, etc.
There's another very crucial point about this that is often overlooked by managers until it's too late - the talent and morale impact of high-interest TD. As someone that has been tasked with maintaining a legacy repository with horrible code in it, I can confirm this from firsthand experience - coding skills tend to atrophy with time, and I've had moments where I've lost almost all passion for the craft. I've found myself having friction with my team because of the mood I'm in, knowing I need to deal with yet another obscure, hardly traceable bug in the legacy mess I've inherited from a bunch of careless contractors and clueless juniors (of whom I have admittedly been).
So how does one measure and quantify TD?
One of the first things that need to be charted down is touch frequency on pieces of a system - how often do you modify existing modules or parts of a system? The ones that get the most attention are your highest-value candidates for a refactor.
The next element in the equation is the amount of what Fowler calls "cruft" in those parts of the system - "deficiencies in internal quality that make it harder than it would ideally be to modify and extend the system further". When you chart the amount of cruft on top of your most frequently touched parts of the system, you get a sort of a heatmap with zones that are both frequently changed AND difficult to change. Those are the areas you want to focus your attention on with the highest priority.
To "sell" this to your managers, you can use a sort of interest-to-income comparison - convert the TD heatmap to a simple ratio for prioritization: interest (time lost per month) ÷ benefit (revenue or product priority). When you explain it that way, it will become clear just how important refactoring can be. At that point it's time to start planning for that refactoring create a sort of "debt register" - list the "owed items" with estimated payoff cost and expected reduction in interest. Naturally, you can't be as precise in this as you can be in finance, but rough estimates should carry your point across effectively enough, provided a reasonable person is on the other end of this conversation.
To make them feel more prepared for it, you can leverage an interest rate proxy - calculate how long a change takes in a specific area of the heatmap before and after refactoring. If you've done a good refactor, the difference should be obvious enough to make the value of refactoring clear.
There are many patterns by which you can approach clearing TD. Here are some of them:
- Refactor as part of the feature - include small refactors in the same ticket. Low coordination cost + immediate ROI (return of investment).
- Definition of Done includes quality - unit tests, docs, and CI checks required before PR merge. Prevents new reckless debt.
- Debt backlog & visible register - treat TD items like standard product backlog items with owners and acceptance criteria. Prioritize against feature work.
- Time-boxed tech-health sprints - scheduled windows where the team focuses on paying down small/medium amounts of debt. Useful when interest rate is continually rising.
- Automate detection - static analysis, dependency scanners, test coverage thresholds, libraries like SonarQube and others - use automation to keep a baseline as much as possible.
- Architectural runway & component ownership - allocate product/engineering ownership for key modules so cruft doesn’t accumulate anonymously - helps with traceability and maintenance.
- Feature toggles & incremental rewrite - for large risky changes, do iterative replacement behind toggles, keeping the system runnable.
- Refinancing choices - sometimes a rewrite or replacement (big refactor) is justifiable, but only after quantifying cost/risk and ensuring resources exist (don’t rewrite for the sake of rewriting).
If area is touched > N times/month and each change costs > M extra hours because of cruft, schedule refactor within next sprint cycle. (Choose N and M to fit your org.)
It's important to contextualize how modern organizations deal with this kind of issue. Some of the approaches include (but are definitely not limited to):
- Visibility & language: use the debt metaphor carefully; clarify whether items are “prudent” or “reckless” and assign expected payoff.
- Decision ownership: require product + tech sign-off for intentional debt (acceptance of consequences + repayment plan).
- Reward systems: incentivize maintaining code health - e.g. make part of performance review or team KPIs, include error budget, tech-health score, or reduction in interest.
- Capacity allocation: dedicate a predictable % of sprint capacity to maintenance (e.g. 10-25%) rather than ad-hoc firefighting. This avoids endless deferral.
- Postmortems & learning: treat large debt incidents as systemic failures; surface root causes and process changes rather than git-blaming individuals.
- Hiring and onboarding: preserve and document architectural rationale so new engineers understand tradeoffs.
It is an inevitable trade-off in software; it becomes a problem only when it’s unmanaged or reckless. Make debt visible, quantify its cost, treat it like a product decision, and allocate predictable capacity to pay it down -then it becomes a lever, not a liability.
202511 - apenwarr
apenwarr.ca