There are only 21 days left in the year, so I thought we’d talk about something fun today—technical debt!
Ok, that was a lie. There’s nothing fun about tech debt, but today’s newsletter might still be helpful if your team, like mine, is using the slow weeks at the end of the year to pay some of it off.
This week, we’ll talk about why tech debt isn’t always a bad thing, a short but powerful book on software design, using cookies in Next.js, and one of my favorite technical talks of all time.
Let’s dive in.
A Perfectly Normal Amount of Tech Debt
Dealing with tech debt sometimes feels like choosing between two equally bad choices.
Taking care of a piece of tech debt right now will slow us down in the short term, but ignoring it will almost certainly slow us down in the long run. It seems that no matter what we do, we can’t win.
To make matters worse, our tech debt backlogs are never-ending piles of work. Even if we find the time to pay some of it off, new tech debt shows up by the time we finish. So what’s even the point?
Whenever one of these infinite to-do lists comes up (both in work and in life), I like to think of this analogy by writer Oliver Burkeman on dealing with information overload. Specifically, this is how Burkeman encourages us to think about our ever-growing “to read” lists:
To return to information overload: this means treating your “to read” pile like a river (a stream that flows past you, and from which you pluck a few choice items, here and there) instead of a bucket (which demands that you empty it).
Treating our list of tech debt as a river instead of a bottomless bucket can be quite liberating—if we know the list will never be empty, there’s absolutely no pressure to check every item off.
But this doesn’t mean we can just take an item at random from the river and get to work. I mean, we can do that, but it’s probably not the most effective way to go about it. With an infinite pile of work and very limited time, deciding what to work on is equally as important as the work itself.
So today, I want to share with you a simple but effective framework for evaluating tech debt. One that will not only help you prioritize your ever-growing tech debt backlog but also help you see that not paying tech debt off can sometimes be the responsible thing to do.
Using Tech Debt Strategically
We should start by talking about how tech debt comes to be in software projects.
We discussed the bad news already—tech debt is largely inevitable and a never-ending pile of work. But there’s also some good news, which is that tech debt is typically not our fault. More often than not, it happens naturally for reasons that are beyond our control.
One of these reasons is time constraints. No matter how clean our code usually is and how comprehensive our designs are, an approaching deadline or production outage will force us to make hard decisions about making something “the right way”, or simply making it work.
Tech debt can also happen because of a lack of understanding. This goes back to Ward Cunningham’s original definition of technical debt, which talks about intentionally taking on a new piece of tech debt, favoring a suboptimal solution until we can develop a better understanding of how the system should look.
In either case, the benefits of tech debt are clear: it allows us to deliver value to our customers sooner, which is always better than delivering it later or not at all. Of course, there’s also a cost for that benefit, which is that the debt we took needs to be paid off; otherwise, we risk it slowing us down or causing issues in the future.
As often happens, there is a tradeoff between the cost and benefits of tech debt. Using tech debt strategically means evaluating this tradeoff for a given piece of tech debt (after all, not all tech debt is created equal), and intentionally deciding how important it is that we pay it off.
Evaluating Tech Debt
There are many different frameworks for evaluating tech debt, but one that I like in particular because of its simplicity is the one the team at Riot Games uses. In short, this framework consists of taking three metrics to evaluate a particular piece of tech debt:
- Impact — how much damage is the tech debt doing? Is it causing bugs, slowing your team down, hurting performance, or maybe a combination of all of those?
- Fix Cost — how much time and effort do you need to spend in order to pay this tech debt off, and what’s the opportunity cost? (i.e., what else could you be doing with your time instead of handling this piece of tech debt?)
- Contagion — how much will the tech debt spread if you don’t fix it as soon as possible?
To better illustrate how this framework works, here are a couple of examples applying it to some of my recent work:
Example: UI Migration Cleanup
One of my recent projects was a series of UI updates to our customer support mailbox product. These updates were behind a feature flag that was turned on for only certain users, which meant that the code for both the “old” and “new” experiences had to coexist for a period of time.
These UI updates were all over the place, so as you might imagine, there were a lot of if/else statements and duplicated components that needed to be cleaned up after the project was complete.
Once the transition phase was over, we were left with the unpaid tech debt of cleaning up the codebase, removing the feature flag, and all the code that supported the old experience. Here’s how the cleanup scored using the Riot framework:
- Impact (3/5) — This piece of tech debt didn’t have a meaningful impact on user experience, but it added a ton of extra code and logic that made the codebase unnecessarily complex.
- Fix Cost (2/5) — This wasn’t the type of cleanup you could do in an hour or two, but it wasn’t a massive endeavor either. Let’s say about a couple of days worth of work.
- Contagion (3/5) — Since there was a significant amount of code to clean up, there was a big chance that someone would start relying on parts of it at some point, which would make the cleanup harder if we decided to postpone it.
Considering all these factors, it made sense to take care of this piece of tech debt as soon as possible. It wasn’t a “drop everything you’re doing and fix this now” kind of urgency, but it was a great candidate to put at the top of the list.
Another Example: React Router Upgrade
My team uses Dependabot to keep our third-party dependencies up to date. Some time ago, we got a new Pull Request from Dependabot to upgrade our version of React Router from v5 to v6.
This major upgrade had several breaking changes, so we couldn’t just merge the PR right away. Here’s how we evaluated this upgrade using the Riot metrics:
- Impact (0/5) — We were perfectly happy with our working version of React Router. Version 5 still received security patches, and we were not missing any features from the latest version. Not being on the latest version had zero impact on our day-to-day work.
- Fix Cost (3/5) — The changes between React Router v5 and v6 were significant—there is even a backward compatibility package and a five-thousand-word migration guide to help with the process—so we knew this would not be an easy upgrade.
- Contagion (1/5) — Our usage of React Router is somewhat encapsulated. We might create new routers or add new routes to our existing apps, but this wouldn’t make the upgrade any harder in the future.
In this case, we decided that keeping this tech debt was more beneficial than paying it off. Yes, there might be a time in the future when we’re forced to upgrade, but if and when that time comes, the effort to pay it off would be roughly the same as it is now.
In general, you can think of the impact and contagion metrics as the interest rate of your tech debt. With a high interest rate, it’s better to pay it off as soon as possible. But if the interest rate is zero (or near zero), postponing it might make more sense.
Remember that the goal is not to eradicate tech debt from your codebase—that’s just impossible. Your job as a maintainer of the codebase is to prioritize your ever-growing list of tech debt so you can focus on the most impactful pieces first.
The key to prioritizing tech debt is making it visible, and using a framework like the one from Riot Games is a great way to do that. I’d suggest you try on your own tech debt—and who knows, you might discover that ignoring that piece of tech debt that has been in your backlog since 2017 was the right thing to do after all.
All the Little Things
All the Little Things by Sandi Metz is one of my favorite technical talks of all time. You might know this already because I’ve referenced it approximately a million times in previous issues of the newsletter, but it deserved a dedicated spot on the Watchlist section, so here it is once more.
In her talk, Sandi walks us through her approach to solving the Gilded Rose, a popular refactoring kata. The talk is truly a refactoring masterclass in less than 40 minutes, and along the way, Sandi teaches us valuable lessons for managing complexity even beyond the scope of refactoring, including the squint test, the evils of duplication, and the importance of object-oriented design.
Here are some of the main lessons from the talk:
- Prefer duplication over the wrong abstraction. This is what the talk is mostly known for, and Sandi has a dedicated blog post that expands on this point. This doesn’t mean we should always choose duplication, but if the choice is between duplication and the wrong abstraction, duplication is far cheaper.
- Reach for open/closed. The Open/Closed Principle (the O in SOLID) is an excellent tool for figuring out what the right abstraction is. If we can add new behavior without modifying existing code, that’s a clear indication that we’ve organized our code in the right way.
- Refactor through complexity. We should learn and lean on the rules of object-oriented design. They’ll teach us to trust that, while the intermediate steps of refactoring might increase complexity, the end result is going to be much simpler.
All the Little Things is almost a decade old at this point, but Sandi’s advice is truly timeless and universal—so don’t worry too much about the fact that this was Railsconf talk, because these lessons apply to any language.
|Watch on YouTube
Links Worth Checking Out
A detailed explainer of the many methods we have available to read and write cookies in Next.js—using the App Router, the Pages Router, in Middleware, or purely client-side.
A thoughtful article from the perspective of an open-source maintainer, explaining the problems that come up when the motivation to work on a project doesn't match the demand there is for it.
Learn the key principles for designing reusable components in this deep dive into React composition patterns.
The question in the book’s title comes from a situation we’ve all experienced before. We want to make a change, but the codebase is messy; should we tidy things up first? You can probably guess what the answer is: it depends, but the book does a great job of explaining what it depends on exactly.
Tidy First? is a very short read (less than 100 pages), and it’s packed with useful advice, organized into three parts:
- Part 1: Tyidings is a collection of mini refactors to make your codebase easier to deal with. These are structure changes that you can apply at any time, without having to set aside a big chunk of time to do them.
- Part 2: Managing covers how and when to tidy. It has some good advice for how to separate your structure changes from your behavior changes, and some helpful guidelines for deciding if you should tidy first, after, later, or never.
- Part 3: Theory is where the heart of the book is, explaining the value of software design from an economic standpoint, and how good design is one of the most effective tools for reducing the cost of software.
While the first two parts of the book are more practical, I found part 3 to be the more interesting one (and it’s where I took most of my notes.) This is personal preference, of course, but if you’re passionate about software design and like to dive into the origins of concepts such as coupling and cohesion, I can highly recommend Tidy First? (both the book and the newsletter of the same name) as your next read.
|Read more on O'Reilly
That’s all for today, friends! Thank you for making it all the way to the end. If you enjoyed the newsletter, it would mean the world to me if you’d share it with your friends and coworkers. (And if you didn't enjoy it, why not share it with an enemy?)
Did someone forward this to you? First of all, tell them how awesome they are, and then consider subscribing to the newsletter to get the next issue right in your inbox.
I read and reply to all of your comments. Feel free to reach out on Twitter or reply to this email directly with any feedback or questions.
Have a great week 👋