Engineering teams often face speed, cost, and bandwidth constraints that lead them to choose quick, simple, and easy solutions over advanced, time-consuming solutions. The result of these decisions is commonly known as technical debt.
What is technical debt?
Technical debt, sometimes also known as code debt, is the cost of rework incurred when teams optimize for short-term requirements, rather than implementing more robust long-term solutions. In short, it is the price teams must pay at some point in the future to make their software efficient, scalable, and maintainable again.
When teams make changes to their codebase, they often need to make changes across many different parts to ensure everything works in unison. Dependencies, components, and services must evolve together to keep production up and running. If teams don’t make all the necessary changes to the codebase at the same time, they should consider any uncompleted items to be technical debt.
What are the most common causes of technical debt?
- Time constraints: When teams need to meet their deadlines, they sometimes opt for shortcuts or simpler implementations. They prioritize speed over correctness due to business demands, incurring technical debt with the hope of taking advantage of a market opportunity to win new customers.
- Changing requirements: Product designs can change while a project is already underway, forcing engineering teams to adapt quickly to meet their deadlines. Changing requirements can be a result of poor planning or sudden changes in the market, such as the Covid-19 pandemic.
- Existing technical debt: When teams don’t have enough time to pay down technical debt, they continue to accrue additional debt on top of their existing debt. Each new feature requires workarounds to integrate with other workarounds. The result is a fragile and brittle codebase, sometimes referred to as software rot.
- Duplicated code: Code with similar functionality in multiple places requires developers to maintain more code. As a result, duplicated code increases the amount of neglected or forgotten code, which can lead to unforeseen problems during development. Duplicated code can also make a codebase harder to navigate and understand.
- Code complexity: Complex code can make it more difficult for developers to make changes, which can lead to complicated edge cases and unexpected behavior.
- Lack of knowledge sharing: Documentation helps team members better understand their team’s code and the decisions that led to each change. Without a deep understanding of their codebase, developers can inadvertently add complexity to the codebase with new workarounds or shortcuts.
- Lack of standards and frameworks: Without team-wide standards, such as linters and test requirements, changes to the codebase may not meet team expectations for structure, clarity, and formatting.
- Long-lived branches: Branches accrue technical debt, which teams must pay down when they finally merge them into the main branch. Long-lived branches typically accrue more debt than short-lived branches.
- Tightly coupled architecture: When changes to a codebase impact many other parts of the codebase, teams are more likely to create one-off solutions or encounter unexpected behavior.
Some types of technical debt can be extremely complex, requiring radical fixes like full migrations or architecture overhauls. Known as design debt, this type of technical debt tracks architectural improvements needed to maintain long-term performance and stability of an application.
Other types of technical debt don’t require sweeping changes, but need continuous time and effort to make code more readable, reusable, and reliable, often through refactoring. Even small issues, such as outdated packages or missing tests, can accumulate as a type of technical debt over time.
The goal of managing technical debt is to accelerate development and improve daily work for engineering teams by making code easier to understand and faster to build upon in the future.
Signs of high vs low technical debt
Signs of technical debt are not always obvious, but there are indicators that can help teams identify and tackle technical debt. Symptoms typically include bug fixes that include new bugs, increasing amounts of data inconsistency, and unstable production environments.
For most engineering teams, the most obvious sign of technical debt is unstable, unreliable, or buggy production environments. At extreme levels of technical debt, bug fixes often introduce new bugs—a clear sign of runaway debt.
Teams will typically see an increase in their mean time to recovery (MTTR), the time it takes them to restore service after an outage, and an increase in their change failure rate, the rate at which deployments to production lead to incidents.
As technical debt increases, teams may also experience difficulty when testing their code. Code complexity and tightly-coupled architectures increase the number and complexity of tests required to fully test their changes. With brittle tests and low test coverage, engineers will feel less confident their testing will catch hard-to-find bugs.
Duplicated code and difficulty reusing components are also signs of growing technical debt. If teams find they are using many similar components across their codebase, it’s likely time to consolidate and refactor their functionality to make fewer components that are highly reusable.
Overall, long-term technical debt can decrease team velocity, leading to longer lead times and lower deployment frequency. When faced with technical debt, developers struggle to integrate their changes quickly and easily. They battle unexpected bugs, complex integrations across the codebase, and complicated code that creates bottlenecks during their day-to-day work.
How to measure technical debt: can you do it with metrics?
By measuring technical debt, teams can make smarter decisions about when and where to pay down debt.
Most engineering teams will estimate the amount of technical debt in their codebase by measuring the amount of time spent on maintenance. According to Stripe’s The Developer Coefficient, the average developer spends 13.5 hours per week managing technical debt. More time spent on maintenance, refactoring, and rearchitecting over the long-term can indicate growing technical debt.
Some engineering teams estimate technical debt by using a ratio called the technical debt ratio. The technical debt ratio is the ratio of the cost to fix a system—the remediation cost—to the cost of developing it—the development cost.
Teams can also use code complexity as a proxy for technical debt. Cyclomatic and cognitive complexity provide teams with insight into how difficult or easy it is to understand their code. Too much complexity can be a sign of growing technical debt and an opportunity to refactor code to be more readable.
Developers can also use afferent and efferent couplings, nesting depth, arity, and depth of inheritance to understand code complexity. Tools such as CodeMetrics and Codalyze can help automate some of these measurements to offer feedback inside code editors and IDEs, such as Visual Studio Code and IntelliJ.
Compounding costs of technical debt
Low levels of technical debt are expected in any software development project. In fact, technical debt is often necessary to move fast and iterate.
Too much technical debt, however, compounds rapidly—growing exponentially if it is not repaid on time.
According to StepSize’s State of Technical Debt:
Engineering teams are under a huge pressure to build and break fast, and many lack time to fix and clean up their codebase. In such situations, technical debt quickly builds up, resulting in a downturn in engineering productivity and significant costs to an organization.
Similar to monetary debt, technical debt accrues interest over time. When faced with unresolved technical debt, engineers build additional workarounds and one-off solutions to handle unfixed issues in their codebase. In short, technical debt leads to more technical debt.
Without enough time to pay down debt, teams create more and more debt over time, until it becomes unmanageable. Development grinds to a halt and teams ask for feature freezes so they can pay down their technical debt. In personal finance, this scenario is known as a debt spiral.
Without visibility into technical debt, teams also struggle to accurately estimate how long projects will take to complete. Missed deadlines force engineers to cut corners and add technical debt, worsening the debt spiral.
Impact of technical debt on developer happiness and performance
On teams with high levels of technical debt, developers face longer delays, requiring extra time to read and understand complex code before making changes. Even simple changes may require various changes across the codebase to overcome technical debt. Unexpected behavior or bugs as a result of technical debt can also lead to lots of rework late in the development cycle.
As a result, developers face lower predictability—unsure exactly what will be required to complete their tasks—and a lack of safety—afraid that their changes will inadvertently cause issues. Team morale suffers, and developers move to companies that allow them to work on more innovative and impactful projects.
Impact of technical debt on company’s delivery speed and ability to stay competitive
When projects are delayed by technical debt and team productivity decreases, the impacts are felt across an organization.
Technical debt can lead to extensive outages, sometimes in violation of a company’s SLAs. Buggy and outdated software negatively affects the user experience and customer satisfaction. It also increases the likelihood of security incidents. Codebase complexity can introduce unintended security holes, leaving companies vulnerable to cybersecurity threats.
Excessive technical debt increases time to market. Developers struggle to integrate new changes and develop features quickly, instead spending too much time resolving issues and bugs.
Less technical debt increases team velocity by making it easier for developers to understand their codebase and confidently add new features.
However, some technical debt can be useful. Low levels of technical debt can help teams both take advantage of near-term market opportunities and avoid over-engineering. They can iterate quickly, release new products, and get faster feedback.
The goal is not to achieve zero technical debt; instead teams should strive to be more deliberate and transparent when taking on new technical debt and when planning how they will pay it down again in the future.
How to start managing technical debt
According to Kenny Rubin, author of Essential Scrum, there are three main types of technical debt: happened-upon technical debt, known technical debt, and targeted technical debt.
Happened-upon technical debt
First, happened-upon technical debt is debt that teams discover in their codebase, such as temporary workarounds, while trying to add new changes. Developers must decide whether to fix it immediately or mark it to be fixed in the future.
Teams should improve visibility into this type of technical debt. Workarounds and short-term solutions work well in certain situations, but teams should document when they create technical debt. This helps other team members understand their code’s limitations and helps them budget time needed to pay down their technical debt in the future.
Code reviews, automated testing, and regular audits can also shine a light on technical debt. Without resorting to shame or blame, teams should consistently seek out technical debt and identify areas for improvement.
Moreover, organizations should provide engineers with the time and tools needed to write and test quality code. Strict time constraints can lead teams to skip over tests and documentation, or ignore compiler and static code analysis warnings. When planning new features, always factor in development time to manage unexpected technical debt.
Known technical debt
Second, known technical debt is documented and visible to a team. They are aware of the debt, its potential impact, and the likely cost of fixing it.
Some known technical debt can be helpful for teams. For example, when building new features, technical debt can help teams avoid over-engineering and over-optimizing too early in the development cycle. Other times, the best design will not be apparent until after starting a project; coding is a learning process and the goal is to continuously improve it.
Teams should aim to consistently and regularly pay down this type of debt. They should budget time each sprint or cycle to reduce technical debt, update legacy code, and replace outdated tools.
Targeted technical debt
Third, targeted technical debt is known debt that is being serviced or paid down.
When fixing technical debt, teams should be fearless. They should experiment and break things just as they would when prototyping or building something new. The result may be a solution more extensible and robust than what the team expected when they first introduced the debt.
Tech debt as an opportunity
Technical debt is an inevitable part of software development. How teams manage it will determine its impact on an organization.
If teams fail to consistently pay down their technical debt, it can slow down development and limit developers when working on new and exciting features. Technical debt can lead to painful feature freezes, lengthy delays, and growing frustration.
When managed well, technical debt is an opportunity for teams to make educated tradeoffs between speed and correctness. It can help teams to get feedback from their customers faster, while budgeting time to identify and fix their code in the future.