Practical Strategies for Managing Technical Debt

Andrew Volkov
Juro Tech
Published in
6 min readApr 9, 2024

Long-term product development success relies on effective and regular management of technical debt. The challenge is to continually evolve the product at a reasonable pace while maintaining a low production bug count. Drawing from the real-life experiences of our engineering team, we’ll explore practical strategies and advice for handling technical debt.

Context matters

It’s important to recognize that tech debt, whether it’s the state of a codebase or an outdated system design, isn’t inherently “bad”. Tech debt should not be prioritized based solely on the code. An “outdated” section of the code may not necessarily need rewriting. Factors beyond the code itself should be considered. These can be evaluated by looking into two aspects: “when” and “why”.

Tech debt becomes relevant when code is about to be changed, there is a high enough risk associated with it, or there is ongoing development overhead. There are additional questions that can help understand the necessity of addressing tech debt:

  • How does it relate to the product roadmap?
  • How does it support the goals of the product, team, or company?
  • What are the risks and implications of not resolving the debt?
  • How to tactfully explain the necessity of addressing this issue to stakeholders without using the terms “tech debt”, “old” or “bad”?

Depending on the answers, we might end up in different situations:

  1. Not related to the roadmap and there is no risk or interest — this is a free loan, the tech debt that we don’t need to address.
  2. Related to the roadmap and relatively small — address it as part of the roadmap work.
  3. Relatively large effort to address, but also involves some risks or interest — run it through prioritization and address (or not) as a separate project.

Let’s dive into more details about the 2nd and 3rd situations.

Addressing smaller pieces of tech debt

There is a situation that allows to bypass prioritization and approval of tech debt work. This “fast track” to tech debt resolution applies when the tech debt is proportionally smaller than feature development work, essentially acting as daily maintenance.

In this situation, tech debt is addressed whenever a new feature is planned, a bug needs fixing, or performance or security issues occur within a certain code area. In other words, when a) the resolution aligns clearly with business value and b) the effort to resolve the technical debt is proportionate to the effort of feature implementation, for example, it falls into the 10% — 50% range.

Resolution of this type of technical debt can be planned either as part of feature tech design and decomposition stage or at the level of a specific task by a responsible engineer.

The tricky situation is when tech debt is discovered during implementation and increasing the scope is not feasible, for example due to already planned release date. Possible ways to handle the situation are

  • Address the most critical technical debt, such as missing tests to ensure existing functionality is not disrupted and new features do not introduce bugs.
  • Create a separate task for the next sprint to address the tech debt and proceed with the change.

This approach is pretty straightforward with bigger features that span multiple sprints. Spending around 10% of the time is usually more than enough to address the tech debt. With smaller bugfixes it might be less obvious how much tech debt to address. Two guiding principles can help:

  1. Some basic test coverage is non-negotiable. You can think about the number of tests that will make you confident enough to release it safely to production.
  2. Is this code area frequently changed, judging purely from historical data? For a more frequently changed area, it’s worth investing more time to address tech debt.

Step by step guide

How to address tech debt during feature development:

  • If there are no tests for the codebase area which is going to be changed, add some. Snapshot (approval) and integration tests are good options, because It’s easier to judge the output of an entire module rather than a specific method, especially if you lack knowledge in that area. Look to fix any flaky tests as well.
  • Address debt related to outdated technology according to your current tech stack. Some examples from our team: JavaScript → TypeScript, React class component → functional component, replacing Lodash with native counterparts, removing or updating outdated 3rd party dependencies.
  • Refactor and cleanup the code. There are good opportunities to clean up code, such as splitting large methods into smaller ones, improving naming, moving code to different files, etc. Look for code health issues like tight coupling, leaky abstractions, weak cohesion, and cyclic dependencies.
  • Implement feature changes.
  • Look for more refactoring or clean-up after logic is changed.

Each of these steps can be a separate PR or a separate commit for review efficiency purposes.

The refactoring part done via Pair Programming promotes codebase knowledge sharing and maybe even some fun.

Addressing larger pieces of tech debt

This kind of technical debt is more difficult to justify because it doesn’t address an immediate business need. Such debt may have a wide scope, often requiring more than one developer’s sprint, making its justification even more complex. An example might be transitioning from one major UI framework to another.

Prioritization framework

There are several prioritization frameworks, such as ICE and RICE, that we use to prioritize new features. For tech debt, we consider the following criteria for better insight: risk, interest, and effort.

This framework helps with the justification and prioritization of tech debt resolution. Let’s take a closer look at the criteria:

Risk. What risks are there if we proceed with not resolving tech debt? Some examples:

  • A critical piece of the application is reaching its end of life, for example, an older version of a database.
  • Outdated dependencies that are no longer supported pose a security risk or may be removed entirely.

Interest. What “interest rate” are we paying for the debt? Does it slow down development? How often and by how much? Is there regular manual work associated with debt? Is it an area of the app that is frequently changed, but the debt is not addressed during these changes?

  • Typical interest can be in the form of slower velocity compared to an up-to-date code. Codebase areas that are changed more often need more attention to quality than stable code areas.
  • Another form of interest can be in the form of repeated manual developer work.
  • If there’s no interest, there’s no need to update the outdated parts of the system that carry significant technical debt — it’s akin to a free loan.

Effort. Have a rough estimate of the scale of work. Is it one sprint? Is it one month? Is it 3 months of work?

After evaluating all the criteria, it’s a decision about whether to proceed and how it stacks up to other tech debt instances. There are always other tech debts waiting to be resolved, so it’s a game of prioritization.

Step by step guide

  1. Run tech debt prioritization through the Risk, Interest, Effort framework.
  2. Get approval from stakeholders for the top item on the list.
  3. Choose or “create” a team to handle tech debt. If the tech debt falls entirely within a particular team’s scope, that team can take it on. However, dedicating a team to this task for an extended period, like a full month, can pose problems due to existing roadmap work. If this is the case, or if the tech debt doesn’t belong to a specific team’s area, forming a cross-team group of engineers may be beneficial. It’s feasible for these engineers to dedicate 20% of their time to the project. Such cross-team groups allow engineers to work on projects they’re interested in, driving them forward while still delivering on the roadmap.
  4. Create a technical design or plan with the goal of aligning the team on the solution and outlining iterations and specific work items. The plan doesn’t need to be extensive, but it should be detailed enough to initiate the project.
  5. Iterate on the work items. Ideally, these should be small enough so that each developer can complete them during the sprint and merge them into the main branch. Working in small steps reduces the risk of rework and merge conflicts.

Takeaway

Look to spend part of development work on addressing tech debt and improving codebase quality, be it adding tests, improving naming, splitting methods or restructuring modules. The amount of work needed, as a percentage of new development work, will greatly vary from case to case, based on codebase health. Allocating time to address technical debt will increase long-term development velocity and decrease the number of bugs in the live system. This will lead to improved customer experience and boost the company’s success.

--

--