Managing Technical Debt

An exploration of techniques for making your technical debt a powerful tool instead of an accidental mess

Jeff Knecht
Slalom Build
12 min readMay 11, 2021

--

Everyone who builds software has, at some point, made a decision to build something the “easy/simple/fast” way instead of a more difficult, but generally preferable way. That choice creates the potential for future rework. We call that potential future rework “technical debt”.

Technical debt is, at worst, a necessary evil; at best, a powerful tool. The difference between the two is mostly determined by the intentionality with which it is introduced to the codebase, as well as how well it’s managed and proactively planned for.

In this article, I will explore 10 steps that I’ve found necessary to ensure that your technical debt will be a powerful tool rather than an accidental mess that requires unplanned reactions.

Be Intentional

Technical debt, like financial debt, is best adopted in an intentional manner. Regular code reviews and pre-defined practices and patterns will keep accidental technical debt to a minimum. This will free you to make the conscious choices that may not be technically ideal, but that may allow you to get your features to market faster than your competition.

Understanding up front that the choices you are making are not ideal will go a long way toward keeping a comfort level with how much technical debt is in your product and balancing that against your tolerance for such debt.

Every individual has a different level of tolerance for technical debt, and it may take a little bit of time for the team to settle on a level of technical debt that everyone can live with. Regular, open discussion among the team about their tolerance levels in comparison to the amount of technical debit in the codebase should be encouraged.

Capture

It’s better to consciously decide to take on technical debt than to stumble upon it, but no matter how you recognize it, it can’t be addressed if it isn’t captured. There are a few important questions to answer in order to effectively manage technical debt.

First of all, what’s the problem? Be specific. Refer to file names, method names, or sections of the code. Use code comments to help you locate the problem in the future, since line numbers are likely to change.

Second: Why is it a problem? Is it a problem because there is only one person on the team who can make heads-or-tails of what the code is doing? Is there a significant performance problem? Does the code violate an established pattern? Or is the code simply hard to read?

Third: What specifically are you proposing to do instead?

Fourth: Why is your proposal better than what is there? How does it solve this problem specifically? Does it also solve other latent issues that have not yet been identified?

Fifth: What are the consequences of doing nothing?

And finally: What else can you share with your teammates or your future self? What alternatives did you consider and why did you land on this one? Where can people find more information about the approach you’re proposing — somewhere else in the codebase, a blog post?

This may all sound like a lot to document, especially for a small change. And, honestly, if the change is that small, why aren’t you following “Boy Scout coding” (ie. leave the code cleaner than you found it) approach to addressing the issue? (If the answer is that your process doesn’t allow for it, then you seriously need to look into changing your processes) For a larger change, though, all of this is going to be helpful in planning future sprints, assessing priority, and sizing the work.

One further piece of advice: If your issue management system supports it, track this work as a separate issue type. Technical debt occupies a space somewhere between a User Story and a Bug. This can be an important distinction in terms of facilitating easier velocity reporting, capacity planning, and management of the levels of debt. Additionally, it may allow you to customize the information you collect that wouldn’t be relevant or useful for those other issue types.

Estimate

Technical debt items should be estimated or sized based on the same scale that feature work is sized. This is often done in Story Points in an agile team. Estimation based on a common metric will facilitate budgeting, velocity reporting and sprint planning activities.

There are some who will argue against using story points to track technical debt due to a concern about the impact it will have on the size of the backlog. If you were able to create a custom issue type (as recommended above), it will be easy to segregate the technical debt backlog from the functional backlog while still providing some consistency for the team in estimation processes and our ability to weave technical debt into the sprint.

There is a small caveat to be aware of here: the longer technical debt goes unaddressed, the bigger and more complex it is likely to become. Sometimes you will get lucky and the debt will go away thanks to a library upgrade or feature deprecation, but you shouldn’t build your technical debt management strategy around this.

Prioritize

Not all technical debt is created equal. Some items will have a much bigger impact than others.

I recommend a simple 4-value prioritization:

  1. Must Do ASAP. This might include critical performance optimizations, infrastructure hardening, or security remediation.
  2. Should do before next release. Items in this category are likely related to the supportability of the system or refactoring that will facilitate implementation of new features planned for the next release.
  3. Can be done any time, even after the next release. An example might be a desire to break up a large module into smaller components, or an upgrade to a library that is not necessarily required for security, performance, or functional reasons.
  4. Gold-plating. Maybe we do it, maybe we don’t. The positive impact of this kind of change just barely is worth the cost of implementation.

Technical debt should be prioritized somewhat independently of the functional feature work, although there is likely to be some overlap and a need to collaborate with the product owner when the changes might impact the user experience (performance optimizations, for example).

Many agile teams leverage a simple stack ranking for prioritization in lieu of this kind of assessment. I like to combine these approaches, because the additional priority tagging because it helps reporting to less technical executives.

In the end, prioritization is always a judgment call.

Plan

In general, Priority 1 work should be completed before any work begins on Priority 2 items. It is important to consider, however, the capacity of the team members who have the right skills to tackle the specific tasks that are being considered. It makes no sense, for example, to hold up lower-priority technical debt for a front-end component simply because there is high-priority item related to the network if the same people aren’t needed for both tasks (as long as that front-end item is one of the higher priority items that a front-end specialist could be working on). One way that teams facilitate this kind of planning is by tagging the backlog items based on the skills required to complete the work.

It is important to consider the size of the items being considered. In any given sprint, it may make more sense to tackle a large number of small, lower-priority items than to go after a single high-priority item. An accumulation of low-hanging fruit can often have a bigger impact than a single item, both in terms of the team’s overall velocity and their morale.

Also worth considering is whether it might make sense to work on a high-priority item later because it relates to feature work planned for a future sprint. Or perhaps it makes sense to take on a lower-priority item sooner because you will be working in that area of code anyway.

Priority 4 items (if you’re following my advice) is where I draw a bit of a harder line. These items shouldn’t really be considered at all until all of the higher priority items are completed unless they are trivial to implement and you happen to be touching that part of the code anyway.

In short, “Priority” is a good guide for establishing the schedule, but it should not be the only factor in your decision-making. Real-world factors need to be taken into consideration at every stage.

Budget

The only way to truly avoid technical debt is to cancel the project. It is a losing game to ignore it or to assume that you don’t need to plan for it.

Set aside a certain amount of time each sprint dedicated to working on technical debt. 15% of your planned sprint velocity is a good starting point. Over time, you will adjust this based on what is appropriate for your team, your delivery timelines, your backlog of high-priority debt, and your tolerance levels.

Note that this is (perhaps, unintuitively) unlikely to slow down the delivery of your product. The team will be addressing technical debt along the way whether it is budgeted or not, and if not budgeted and planned for, it will simply show up as lower velocity that is more difficult to predict.

By budgeting for it, your feature velocity will remain more stable throughout the project delivery, and your engineering team will be happier because they can plan ahead for keeping the codebase clean. All of this combines to give you a much clearer picture of when you’ll actually be ready for your next release and how supportable it will be.

My advice: have this conversation with your product owner early, and plan for it right from the beginning.

Habitualize

True craftspeople in every walk of life tend to follow a mantra of “clean as you go.” There’s a good reason for this. It makes the work easier when you are working in a clean environment that is set up well for the work to proceed.

My advice for tackling technical debt: work it into each and every sprint.

Avoid working on technical debt only in “hardening” sprints if you can. I have seen some teams create a regular cadence of hardening sprints (say, every fourth or fifth sprint). This is a great idea when combined with a regular in-sprint budget for addressing technical debt along the way (although, I would admit it is probably overkill). If you can’t get agreement to weave the work in to every sprint, then doing it during hardening sprints is better than not planning for technical debt at all. However, this approach can make things more difficult for engineering teams.

The first difficulty can be convincing the product owner to shut down new feature delivery on a regular basis. It is typically much easier to establish a sustainable velocity that addresses code quality on an ongoing basis than it is to convince your business partners that after running at a high velocity for four or five sprints, we’re going to pause and rewrite stuff that is, in their view, already working fine.

The second difficulty can be that, while significant tech debt is outstanding, all of the feature work for those sprints may be encumbered by that same technical debt. The teams are working around patterns that they know are not ideal, and they are actually adding to the pile of problematic code.

The third difficulty that this approach can present is that there is a cost in terms of distance from the problem that is borne by the team when they are forced to wait several sprints from when they noticed the need for a change. By the time they get around to addressing it, the problem has likely become bigger, or they may have lost momentum on the fix. It is probably going to take longer to complete the work than it would have if they could have addressed the issue sooner.

The fourth difficulty that can occur is that defect resolution and performance enhancement may dominate hardening sprints. This may not leave much time for addressing technical debt.

A further risk is that the team may decide that “we don’t have time for a hardening sprint.” A looming project deadline creates a strong incentive to eliminate any “unnecessary” work, and hardening sprints are often an early victim in the search for efficiency.

Monitor

Now that you’re executing on your plan, you can focus on monitoring the technical debt that is being created and the rate at which it is being paid down.

On a sprint-by-sprint basis, keep track of how much tech debt has been created, removed, completed, and reprioritized. Track it by priority, so you can ensure the team is working on the right things at the right time and so that you can see whether things are getting out of hand.

Ongoing monitoring of this information is important for ensuring that your sprint budget is appropriate and sufficient to manage the technical debt backlog (especially the higher-priority items). Knowing how the backlog is changing over time can provide insight as to whether we should be planning more or less time in each sprint to address the highest priority items.

An ever-growing technical debt backlog may be an indicator that the team is cutting too many corners in order to deliver features at an unsustainable velocity. Conversely, a rapidly shrinking backlog may be an indicator that your budget for technical debt is more aggressive than it needs to be. Either case might represent a long-term risk, and this information will help facilitate a conversation with the product owner.

Review

One particularly challenging aspect of managing technical debt, aside from its somewhat unpredictable nature, is that the priority and size of existing technical debt items can change over time. What was once a small, relatively unimportant tweak to a couple lines of code might easily grow into major surgery that needs to be performed immediately, otherwise it will be impossible to implement the next major feature.

It is important for the technical leadership of the team to remain vigilant and to review and re-prioritize the technical debt backlog regularly in order to identify and address this sort of issue before it catches them by surprise.

Adjust

As with all Agile delivery processes, we need to regularly adjust based on what is working well and what can be improved.

The budget for technical debt work in each sprint should be adjusted periodically to reflect the current state of the backlog. A good approach is to identify a level of technical debt that the team is comfortable with (perhaps as a percentage of the user story backlog), and then adjust your budget so that the level of outstanding high priority debt remains at a relatively stable level. Additionally, there may be times where you need to pull budget forward, and there may be times you need to defer technical debt work in favor of other work.

The estimates of the technical debt items should be adjusted periodically as the size and complexity of the system changes, and as significant architectural decisions are introduced.

The structure of the information collected when technical debt items are added to the backlog should be adjusted as the team decides what information is valuable and what is not.

Priorities should be adjusted periodically. Something that was a low priority when it was first identified may become a top priority as we near the production release or as functional priorities shift.

Remaining flexible and vigilant not only for anything new, but also of the items that remain in the backlog is a critical component to managing technical debt, rather than reacting to it.

What to expect when you’re expecting technical debt

I am often asked, “How much technical debt should I expect?” This is a bit like asking, “how long is a string?”

The real answer is less quantifiable than it is qualifiable. Is the level of technical debt causing the team to move too slowly to satisfy the needs of the business? Is the kind of technical debt that’s in the backlog causing sleepless nights? Has anyone rage-quit because “the codebase is hot garbage?” If the answer to any of these questions is “yes”, then you have a problem.

What makes it difficult to quantify is that (a) every codebase is different (Greenfield vs legacy codebases, for example), (b) every technical leader has a different tolerance level for technical debt, and (c) the makeup, composition, and rate of change of the technical debt backlog is going to be different at different stages in a project.

Early development phases will likely see wide, sporadic swings in both the quantity and priority of technical debt. The team may do something “quick and dirty” in order to get the product owner an early look at a feature. It’s best to temper your expectations of a pristine codebase at this phase, and let things happen organically. The important thing in this phase is for the team to develop the habit of capturing the technical debt as soon as soon as it is created or noticed.

In the middle phase of development, which is where the bulk of the time is spent for the development team, you should see the curve level off. In a healthy project, new technical debt is likely to be created at a similar pace to which it is being addressed. There will likely be blips in the curve as the team decides to undertake large refactoring efforts, but if you zoom out far enough, the curve should remain predominantly level. If not, you may need to adjust your budget to help flatten the curve.

Toward the end of the project, a higher percentage of your backlog will be dedicated to technical debt. This happens as a natural consequence of the number of outstanding new features dropping and the quantity of technical debt items becoming a larger and larger portion of the remaining backlog.

Conclusion

In this article, I’ve laid out a process for managing technical debt that should make it more visible and easier to manage. This process will, in turn, make the team’s functional delivery velocity more stable and predictable, increase the quality of the codebase, and help maximize the satisfaction of the engineers responsible for maintaining and building on that codebase.

--

--

Jeff Knecht
Slalom Build

Software delivery leader. Builder. Craftsman. People advocate. Agile evangelist.