managing technical debt

Managing Technical Debt: Etiquette, Tips, and Techniques

Technical debt is a type of debt that we build up over time when working on any project. This debt is either gained accidentally, usually from bit rot; or intentionally, with the expressed intent that it was a hack to get a feature out the door.

There are many moving parts involved in product delivery. Ideally, we never utilize developer shortcuts or compromises. And even when we knowingly take shortcuts, the intention is always to come back and clean this up eventually.

In an ideal world, we implement engineering solutions correctly the first time. But in a build-fast-and-iterate world, deadlines can affect best practices.

In this article, we’ll be discussing intentional technical debt.

Here are some of the reasons we find ourselves building up intentional tech debt:

  • We need to have feature X shipped by Y date. However, the only way we can do this is by hardcoding as a plain file instead of figuring out a final data structure we can use to store it in the database where it can be adjusted on the fly.
  • Working too fast to explain what we’re doing. Sometimes we write code that isn’t easy to follow or debug. It’s important to leave enough breadcrumbs to explain what the code is trying to accomplish. This saves a lot of time when we need to touch the code again a few years later.
  • Staying in build mode, and never getting into maintenance. We don’t manage our dependencies and keep them up-to-date.
  • Forgetting to clean up after ourselves. We leave commented-out or debugging code in the code base instead of deleting it.

In any given situation, these are completely valid reasons. But we have to be more proactive on cleaning up any mess we leave behind (or inherit.)

It’s good etiquette to make it easier for your peers to do their work as well.

If we are going to avoid technical debt, we need to recognize these behaviors as mistakes. We have to fight for what we think needs to be done.

We often find the most effective approaches to managing tech debt in Open Source projects.

Let’s discuss some of these approaches.

Manage Technical Debt with Refactoring

As time permits, use refactoring to improve things. You don’t have to refactor all the time. Make the incremental improvements that help you and those who follow you to understand the code a little more each time you look at it.

I think we are slowly moving away from accepting “cryptic logic” and leaning towards code that is more readable, while making compromises for optimizations when the code is a “hot” path.

So it’s great anytime you have the chance to re-write a 200+ line function and trim it down to its bare necessities. This could result in a function that is only 20 lines or so and much more readable.

There are a few ways you could perform this type of refactor: simply renaming some variables to make more sense, or going in and re-writing a portion of a function to be more readable.

This incremental refactoring builds up over time and enables anyone who touches the code going forward to be more confident and efficient.

Here is a simple example that moves code around a bit and defines some additional variables to improve readability. These types of changes are applicable to functions that are very complex, contain multiple conditionals, and nested logic.

In these cases, it’s better to rethink your approach and break them into smaller functions that are readable, reusable, and/or easy to test.

// OLD
function cache(key, data, expiresIn = 0 /* seconds */) {
  if (!key || !data || !_.isObject(data)) {
    throw Error('Invalid Input');
  }
  
  if (expiresIn === 0 || _.isEmpty(data)) {
    return false;
  }
  
  return true;
)

// NEW
function cache(key, data, expiresIn = 0 /* seconds */) {
  const isInvalidInput = !key || !data || !_.isObject(data); /* The idea here is to lower the amount effort required
                                                                to understand a conditional statement by using named
                                                                variables that explain what we are checking by making
                                                                it readable. We can understand that all of this logic
                                                                is to determine that the input is valid. */
  const dontCache = expiresIn === 0 || _.isEmpty(data);      /* This is a bit more superficial, but sometimes having
                                                                a named variable can make reading conditionals easier. */
  if (isInvalidInput) {
    throw Error('Invalid Input');
  }
  
  if (dontCache || _.isEmpty(data)) {
    return false;
  }
  
  return true;
}

Again, think in incremental steps. If you refactor everything all at once, you are giving your code reviewers extra work. And it will take longer for your code to get approved and merged.

It is perfectly fine to create multiple small PRs. The first refactoring doesn’t need to totally overhaul everything to make it perfect.

Sometimes, the first refactoring just breaks things up a bit and renames things. And the next time somebody looks at it they understand it better, allowing them to simplify it further.

Manage Your Technical Debt with Automation Services

There are times that we forget dependencies also need love and care. It seems like we only remember to update them when security vulnerabilities arise. Automate the mundane tasks so you can spend time doing the more important work.

Imagine getting even ten minutes of your life back by automating these steps, allowing a machine to perform them in seconds:

  • Create a new branch
  • Update the version of a dependency
  • Push the changes up
  • Create a Pull Request.

You can use services such as DependabotGreenkeeper, and snyk. They will monitor your code base and provide you with updates that contain the latest releases.

These services also provide the ability to automatically create a PR against your development branch. The PR will run the latest dependency updates against your test suites to ensure nothing brakes. This enables you spend your precious time on more significant tasks.

Manage Your Technical Debt by Leveraging CI

This builds upon the previous statement, but with a different approach. I think language versions is something we often forget until a version has been EOL’ed.

However, when working in a language/environment where the versions bump very quickly within a year this tends to get a bit tricky. This doesn’t mean that it won’t work for languages that take a long time to introduce changes. We can still take away how Continuous Integration is used either way.

manage technical debt using CI

When we look at open source and complex projects, we find them running CI builds against a large variety of environments. This is necessary, as it can help minimize the amount of issues developers of our code would face.

We are offloading a lot of our checks to our test service instead, and we can have any end-user provide any additional feedback when they run into special cases. The bare minimum for projects should be current version and the next version.

The main reason we use this test service is to be informed of any potential issue in an automated way. If we are alerted of an event that could break something, we can act on the issue as it arises, instead of as part of a routine check at some unknown point in the future.

If you’ve ever had to update dependencies on an old project, you know that it’s a very painful and time-consuming process.

Since we should already have CI as part of the development pipeline/process, this provides us with basic tools to help surface up warnings and errors earlier in the process and gradually over time rather than unexpectedly trying to resolve every little thing at the same time. The same can be said about security vulnerabilities.

Manage Your Tech Debt by Leaving Breadcrumbs

It’s important to leave breadcrumbs both in the code and in your bug/issue tracker. This is so that you have context as to what was happening at the time and the thought process of what we should be doing to handle it when we have the time. Something I’ve been doing is leaving TODO comments with a link to the issue/task that is associated with cleaning it up.

This way, we all reap the following benefits:

  • We help prioritize the refactoring and code cleanup for future sprints. These TODOs should be addressed as soon as possible to ensure we don’t let the problematic code accumulate. While it’s important to continue to build out new features for a product, it’s equally important to improve the code and its reliability. Tests can only go so far until we reach an edge case we know about but couldn’t handle because we compromised to ship it instead of building properly. Keep hard facts recorded in the ticket logged to use as ammunition.
  • Add a link to a ticket to give the next engineer more valuable context. They will have all the information they need without having to ask around. Not only do we often leave out details in TODOcomments, but they also quickly become out-of-date. Adding it as a ticket gives us the ability to both track it in our sprint priorities and update it accordingly in a predictable location.
  • A ticket filed is a ticket tracked. You get into the mindset that since a ticket has been filed it becomes work that should be completed and tracked. This means that the “debt” will be addressed and follow the same rigorous process you have in place for any work normally done.

Make good use of those breadcrumbs by setting a Debt Day.

A “Debt Day” can occur on a regular basis. Perhaps your Debt Day will be held each sprint, every other sprint, or on a schedule that fits into your process.

This day is dedicated to resolving any outstanding debt filed in the backlog and to keep the tech debt down to the minimum. You can also use this as an additional metric to understand your velocity better.

Also, if there is any downtime and none of your co-workers requires your assistance to get the sprint closed out, this provides another list of possible work that can be done to improve the overall quality of the code base.

Conclusion

It’s not new and it’s not exciting, the process of grooming code and managing technical debt. But when you leave these tidbits as afterthoughts, your work gets messy and your productivity plummets.

Managing your tech debt as part of the development process from the very beginning will resolve some heartache. And, you can work on it incrementally, improving the situation one step at a time.

We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.