---
title: Addressing technical debt
teaser: Discover strategies for addressing technical debt and improving code quality
  through refactoring and collaboration.
tags: ruby on rails,technical debt,refactoring,code audit
author: Joël Quenneville
published_on: 2019-11-04
---

Technical debt is something that is present in all organizations. Incurring it
can help free up the time of your dev team to work on high-priority features but
if left unaddressed in the long run it can destroy you from the inside.

## What is technical debt

In it's classical formulation, **technical debt** is described as intentionally
taking shortcuts and cutting corners in code quality in order speed up
development. While this can be valuable in the short term, over the long term it
makes the overall codebase more bug-prone and future features slower to develop.
Eventually you "pay back" the debt by cleaning up the original code.

The financial metaphor of debt is used as a mental model to understand the
[tradeoffs around an application's code quality]. You don't have to be at 100%
quality all the time (you can't). Sometimes there are strategic reasons to let
quality dip temporarily.

[tradeoffs around an application's code quality]: https://thoughtbot.com/blog/velocity-vs-quality-how-do-developers-and-founders-meet-in-the-middle

## Identifying Hotspots

Unlike real debt, technical debt isn't usually tracked and accounted. So how do
you even know what parts of the code might need some attention?

Identifying and paying down technical debt is something we do a lot of at
thoughtbot, both as part of our [code audit] service, and a day-to-day part of
of our regular engagements.

The first, and perhaps obvious step, is to **talk to your dev team**. Ask them
what parts of the code make it hard for them to add new features? What parts of
the code are scary to change? They know the parts of your codebase where "here
be dragons". Newcomers to the team and junior developers can be particularly
helpful here.  What parts of the code are confusing? What areas are
intimidating?

In addition to talking with your team, here are some common tech debt hotspots
to look out for.

Check the state of your **test suite**. Do you have one? If so do you have a
reasonable amount of coverage (it doesn't have to be 100%)? Do all the tests
currently pass on your `master` branch? [A test suite is critical] to giving you
confidence in your code and the changes improvements you're going to make.
Because this doesn't have an immediate impact on client-facing behavior it's one
of the first place where corners are cut.

Check the state of your project's **documentation**. This can be code-level
comments, setup instructions, or even outlines of your architecture. Sometimes,
documentation is straight up absent. Other times it's poorly written or hard to
access and navigate. Ask yourself: How hard is it for teammates that recently
joined the project to become productive?

Search for unnecessary sources of complexity in your application. The part of
the code that handles your **core business logic** is a good place to start. In
object-oriented systems, a lot of this complexity usually accretes around a
single object colloquially known as the system's [god object].

Keep an eye open for [anti-patterns] specific to your paradigm, language, and
framework. In a Rails app for example, you might look for heavy uses of
metaprogramming, database calls from the view layer, or monkey-patching.

Finally, check to see your code dependencies are **up to date**.
Procrastinating on language or framework major version upgrades can cost you a
lot of extra development time in the long term. This is an underrated source
source of tech debt so we'll explore it in more depth in an upcoming section.

[code audit]: https://thoughtbot.com/services/ruby-on-rails-development
[god object]: https://exceptionnotfound.net/god-objects-the-daily-software-anti-pattern/
[anti-patterns]: https://thoughtbot.com/blog/ai-in-focus:refactoring-rails
[A test suite is critical]: https://thoughtbot.com/blog/a-journey-towards-better-testing-practices

## An aside on performance

> Make it work, make it right, make it fast
>
> -- Kent Beck

It's common to ship the first version of a feature that uses a naive
implementation with no performance improvements. Compromising on quality to ship
faster, that sounds like technical debt! There's a whole different set of
tradeoffs however.

Unlike traditional tech debt, unoptimized code has **no cost** if it hasn't
become a bottleneck. This makes it more like a zero-interest loan that you only
need to pay off if you become really successful. That's a pretty good deal!
Start simple and move quickly. You can always [scale your app] once it becomes
popular.

Beware that performance and scaling changes often introduce new complexity into
your application and can themselves be the cause of new technical debt.

[scale your app]: https://thoughtbot.com/blog/technical-considerations-when-scaling-your-application

## Case Study: Software Upgrades

Software that falls behind on updates progressively becomes harder to change and
deploy. This is often referred to as **bitrot**. Software is not static nor
does it inhabit a static world. Everything is constantly undergoing change, from
the very languages the code is written in, to tools used to build the software,
all the way to the platforms customers use to consume it.

It's easy to procrastinate on updates. After all, why put in all that effort
just to have code that does exactly the same thing as it already does today?

This is particularly pernicious because putting off updates is like the old
proverb about boiling a frog. At first, day-to-day development will continue to
feel mostly the same. As time goes on, you start encountering a plugin that is
no longer compatible here, a library that no longer supports you there. It
starts as a minor annoyance but keeps growing until eventually you can't ignore
it anymore. By now, doing the work of an upgrade will be very expensive.

It's often tempting to put off the work of an upgrade by forking unsupported
version of packages, frameworks, or even languages. This ends up being really
costly as now you're taking on even more maintenance burden. Even worse, now
your code can start to diverge quite a bit from standard use to work with your
custom versions of these dependencies. When you finally do upgrade it's going to
_much_ more expensive.

Keeping with the metaphor of technical debt, this is like taking out a
high-interest loan to prevent from defaulting on an existing debt.

At thoughtbot, we've worked on many upgrades for our clients over the years.
We've found that upgrading sooner is much cheaper than later. Additionally, work
on an upgrade doesn't have to be all or nothing. It's often possible to make
changes to your app now that will make it more forwards compatible with the
version you're trying to upgrade to. This can be done by creating shims for new
APIs. Sometimes there's even official tooling for this, such as Rails'
[`strong_parameters`] gem that allowed you to use the Rails 4 strong parameters
API in Rails 3 applications. Introducing this gem isn't as much work as a full
upgrade would be. When the time comes and you're ready to completely switch
over, the task is much easier because a lot of your code is already compatible.

[`strong_parameters`]: https://github.com/rails/strong_parameters

## Prioritizing fixes

You know you have technical debt. You've identified the areas that need
improvement. Now how do you prioritize these changes?

Common financial advice is to pay off high-interest credit cards before trying
to pay off your mortgage. The same applies to technical debt. What  parts of
your code will be most expensive to fix next week rather than today? Which fixes
are blocking or slowing down high-priority feature work?

Comparing the **complexity** and **churn** (frequency of changes) of the files
in a project is a technique popularized by [Michael Feathers] to identify
high-priority improvement opportunities in your application. Not all complex
files are important to change _now_. Low-quality code only imposes a cost when
you need to modify, extend, or debug it. A subsystem that is very complex but
never has to be modified has a very low "interest rate". Instead you'll want to
focus on files that are both complex and change a lot. These are prime
candidates for [refactoring].

Once you know which improvements are high-priority, it's time to schedule them.

## Scheduling the work

Ideally, you can integrate code quality improvements into day-to-day work. At
thoughtbot we will take time to improve any code that we've needed to modify as
part of implementing a particular feature. This "slow and steady wins the race"
approach encourages smaller, iterative changes to fixing technical debt. Even
seemingly monolithic fixes like language or framework version upgrades can often
be decomposed into smaller steps that can shipped without needing to do the full
upgrade all at once.

Onboarding and project setup is a great example of this kind of day-to-day
improvement approach. When a newcomer to the project tries to get set up, they
will inevitably run into issues. Are there some setup steps that aren't
documented? Open a PR against the README. If the steps can be automated and
added to the setup script, that's even better!

Not all tech debt improvements can be folded into day-to-day work. Occasionally
you will encounter large enough chunk of technical debt that may require a more
targeted and sustained approach to paying it back. You might devote a week to
cleaning up a particular part of the code. For bigger problems, you might create
a small team to address them. This is the [approach GitHub took when doing a
massive Rails upgrade].

Make sure that these more sustained efforts have clearly defined goals like "get
us onto the latest version of our language" or "[break up] a particular big
gnarly part of our code into smaller components". This tends to yield better
results than more unfocused approaches like having every fifth sprint be
designated a "fix technical debt" sprint.

[Michael Feathers]: https://www.stickyminds.com/article/getting-empirical-about-refactoring
[refactoring]: https://thoughtbot.com/blog/reasons-not-to-refactor
[approach GitHub took when doing a massive Rails upgrade]: https://github.blog/2018-09-28-upgrading-github-from-rails-3-2-to-5-2/
[break up]: https://www.sandimetz.com/blog/2017/9/13/breaking-up-the-behemoth

## Conclusion

It's worth noting that there is _no such thing as perfect code_. You can't put
in a lot of work, eliminate all the tech debt, and then go back to feature work.
Instead, you and your team will have to constantly evaluate whether a particular
quality improvement is worth doing now. As every feature is developed, you will
to decide when "good enough" is good enough.

**Need help tackling technical debt in your Rails application?** Our experienced team can help you assess your codebase, create a refactoring strategy, and implement best practices to improve code quality and reduce maintenance burden. [Let's talk about your Rails project](https://thoughtbot.com/services/ruby-on-rails-development).
