What Is Technical Debt?
Technical Debt, also known as design debt or code debt, is the conceptualization of the potential or definitive cost incurred from prioritizing a fast delivery over code quality. We often think about this in terms of a monetary value but cost in this case can also be represented by work hours, negative user experiences, or complexity of code. Sometimes it is _necessary_ to accept and introduce technical debt in order to provide immediate value to a client and their stakeholders. However it must be understood that this debt will present a pain point down the road that will need to be addressed to maintain a healthy and limber application. We call addressing these issues "paying down" technical debt. In any event, it is an important skill to be able to recognize the occurrence of technical debt and understand how it affects our application. It is equally important to evaluate our decisions when architecting and writing code in order to identify the potential for the accrual of technical debt.
The following aims to help you understand not only what technical debt is, but how to identify it, and how and when to resolve it.
Is Your Application Past Due?
An academic understanding of technical debt is valuable, but how can it be applied when auditing your application to determine if technical debt is present? Technical debt has many faces and can affect your application in a variety of ways. The diagnostic questions below are a jumping-off point to determine if your application is in debt.
Reusability & Extensibility
- Are there special cases that are required in order for a certain piece of code to handle its core operation?
- Is there evidence of extensive duplication?
- Is it difficult to extend a class or module without introducing regressions?
Code Quality & Complexity
- Does the complexity of the code exceed the expected complexity required for the work performed by that code?
- How difficult is it to understand the core logic of the application? Is it difficult because it is complicated by nature, or because it is poorly organized?
- Can you run static analysis, linting, and security checks without silencing warnings?
- Is it difficult to write and execute tests for the code?
- Do certain areas of the codebase lack test coverage altogether?
- Are there identifiable performance bottlenecks in the application?
- Are you version-locked in any way?
- Is it difficult to keep your dependencies up-to-date? Does updating a dependency break functionality?
Paying Down Your Tech Debt
Before diving into the specifics, there are a few high-level concepts to keep in mind while you plan and address technical debt.
Setting reasonable and achievable goals will be more effective than trying to resolve all of the accumulated debt at once. For example, "Refactor all the tests" isn't reasonable. On the other hand, "This one spec file uses an outdated syntax, and I can fix it by converting the current cases to use WHATEVER methodology" is much more reasonable and actionable.
It is our responsibility as engineers to communicate the technical debt clearly and accurately to our clients. Performing this type of work often requires buy-in from product managers, engineering managers, sales staff, etc. If you take the time to address tech debt, it's likely that you'll need to de-prioritize other tasks. The only way to allot this time is by getting buy-in from the stakeholders.
Now that you are prepped to pay off technical debt, how should you go about it?
Reusability & Extensibility
- Reorganize classes into modules, libraries, or packages. If you have got a long list of files without any structure, you're increasing the cognitive load required to fully understand the application.
- Remove code wherever possible. Is it unused? Delete it. "Saving" old code for future use usually results in code that is never used, or in code that no longer does what we want it to do. See YAGNI.
- Extract duplicated work into independent, testable classes. Not only will this DRY up the codebase and make it easier to navigate, but unit testing the work separately will ensure that this piece of code is functioning exactly how you expect it to.
Code Quality & Complexity
- Create pull requests (PRs) and review your team's code. The old adage stands: two heads are better than one. Your teammates may catch code that has the potential to incur technical debt that you may have missed. It also provides a forum to document discussions about reasoning and alternative approaches.
- Use linters and apply them consistently. They make it much easier to establish code norms that need to be followed, which helps to minimize technical debt.
- Ensure that you have sufficient test coverage. You can use code coverage analysis tools like SimpleCov to gain insight into gaps in your coverage.
- Write clear, atomic tests. If your application includes long and/or complex tests it may be beneficial to break them down to increase readability. Additionally - as discussed earlier - if you find it difficult to test a particular piece of code, this may be an indicator of technical debt in the code itself.
- Audit the queries in your application. Are there any that are unnecessarily complex or inefficient? Do you have any N+1's? A tool like Bullet can help to find inefficient queries at the outset and prevent issues in production.
- Leverage code-monitoring tools and platforms like New Relic, such that bottlenecks are identified early and easily traceable.
- Remove comments in favor of descriptive method/variable names, or include context in your tests and commit messages. If code comments must be included, ensure that they are clear and remain up-to-date. A rule of thumb for comments, should you require them: use them to explain why, not what. If the functionality of the code (the what) cannot be understood, that is an indicator of technical debt.
- Update your dependencies aggressively. The sooner you find out where you're blocked, the easier it is to schedule time to resolve the situation.
Making Regular Payments or a Lump Sum?
The last major piece to addressing technical debt is: when? Once you know it's there, it is instinctual to want to fix everything as soon as possible. If that is feasible, then great! However, in most cases, tech debt must be addressed systematically and will involve careful planning. The ability to execute depends on a variety of factors including the stage of the project, the capacity of the team, budget, etc. Below are some thoughts on the right time(s) to handle technical debt.
Before You See It, Sometimes
- Does the current ticket touch a file with methods that aren't used anywhere in the codebase? Review them, remove them, and document the reason for their removal.
- Is the code you're writing copying identical functionality from elsewhere in the application? Consider consolidating it. If it requires a refactor down the line, you'll have a single source for the behavior, and will have an easier time implementing upgrades.
- Is there something in a current PR that seems off? Mention it. Shipping consistent, high-quality code that goes through thorough peer review is a great way to mitigate technical debt, especially when it uses Continuous Integration (CI) style checks.
When You See It Affects Users
- This is often the case for performance bottlenecks. If a user can't use a page because, say, a whole bunch of database columns are missing indices, that's tech debt that requires a rapid fix. In the process, it's probably a good idea to look for ways in which the code causing the bottleneck could be improved to mitigate the problem as well.
When Planned Work Will Be Affected Soon
- This requires some foreknowledge of tech debt present in the system. Keeping a list of known issues somewhere in a shared folder or in your project management software is a good idea.
- Is the feature you're working on going to interact with known issues? For instance, are you upgrading the UI for some reason, and need to migrate off of an EOL library in the process?
Staying (Mostly) Debt-Free
Technical debt can considered the occasional cost of doing business. Sometimes, in order to deliver value to clients and stakeholders we need to prioritize speedy delivery over code quality, as difficult as that may feel. However, it is important to be aware of when tech debt can arise (or has already arisen) and to communicate that finding with your team. Documentation can help to keep debt-related tasks in play and ensure that they do not slip through the cracks. As always, we can leverage tools provided to us from the open-source community to ensure that our codebase is healthy and consistent. Lastly, always assess your application as a unique entity when determining when and how to address technical debt, as it may not be plausible to address it during any given cycle.
A big thanks to The Gnar's Nick Marshall, who contributed many wonderful thoughts and brilliant insights to this post.