So there are a lot of posts out there on the topic of technical debt, and to be honest it’s a perfect example of a term that has been abused and given all kinds of unintended meanings. One thing I wanted to accomplish here was tackle the topic of what Technical Debt actually is, and some strategies to manage it.
So if you look out there at almost any blog on financial planning of any kind, they will tell you that debt is a very dangerous thing, and it can devour your income over time because of high interest rates, and take your paycheck and kill it with the death of a thousand cuts. The biggest problem with Debt is that it can creep up on you.
For the sake of an example, let’s look at a simple scenario. If you get a credit card with a balance of $500 and an interest rate of 15%, and let’s say you can only pay $200 a month on it. Each month the balance goes up by $75, so that means your only really paying $125 dollars. So that credit card would take you a lot longer to pay off, and you will end up paying when more than $500 on that debt. And that whole time you lose the ability to generate income with the money being thrown into the “credit card pit.”
Now this is not a financial blog, cause god knows I’m no expert. But I do want to use the above to illustrate a point. In software engineering, we have this concept of “Technical Debt” and if we look, wikipedia, actually has a pretty good definition of it. The definition is:
“Technical debt (also known as design debt or code debt, but can be also related to other technical endeavors) is a concept in software development that reflects the implied cost of additional rework caused by choosing an easy (limited) solution now instead of using a better approach that would take longer.”Wikipedia / Technopedia
Now, I like that definition, but I will change it slightly, I don’t believe that technical debt is limited to only choosing the “Easy” solution, but can actually be caused by any constraint that causes a decision to be made. And I would give the following reasons:
- Budget: Every project has a budget, whether that’s in time and dollars, everyone would do things differently if they never had to worry about time or money, but that’s not how the world works.
- Delivery / Contractual Timelines: Sometimes you just have to ship it, and requirements force you to ship something that is not perfect or the way you want it. And you plan to get to it later.
- Complexity: One of the biggest things I’ve seen that some developers do to avoid technical debt, actually causes it to be significantly worse. Many architects and developers will over complicate their design just for the sake of “making it easier to maintain”, but this can cause further issues later by making the code complicated to test, maintain, and change because baseline assumptions turned out to be wrong.
- Competing Requirements: Sometimes you just get requirements that force your hand in such a way that you can’t avoid practices and patterns you normally would have avoided. For example, you might want to take advantage of the cloud, but be stuck with requirements for OnPrem.
- Skill level: I’m going to flat out say this, when I look at old code that I’ve written, but haven’t touched in a long time. I usually think to myself “Man, I was a terrible programmer.” And to quote Red vs Blue, “Were you smart 10 years go, no you were an idiot…and the truth is your just as much of an idiot now, it will just take you 10 more years to realize it.”
- Available technology: Look at the end of the day, we can only build with the tools we have, and most of the good programmers I know try really hard to have crystal balls, but sometimes our predictions don’t pan out.
Now I’m going to hazard a guess, but if you go back and look at most of the refactors you’ve taken on, I’ll bet that more than a few were up there in that list. And if not, I’d love to hear your reasons in the comments below.
So to a certain extent, the nature of technical debt is impossible to avoid, but what we can do is take strategies to minimize it. We need to accept that technical debt will happen, but if we do a few practices, I find that it can be a lot more manageable. And I’m going to give a few tricks I’ve learned over the years here.
Tip #1 – Create backlog stories around debt, and tag them if possible
Let’s be honest, most development shops maintain a backlog of some kind, and in that backlog are items, user stories, or some kind of baseline unit for work.
Now stop me if you heard this one before, but the following events occur:
- Developer gets new story for a feature.
- Developer designs feature with architecture and business analyst.
- Developer codes feature.
- Time lines or priorities shift, and developer doesn’t get to put as much work in as planned this sprint.
- Developer gets code to point that meets the requirements, but has a few things they wish they did differently.
- Developer ships code, maybe (if your lucky) makes notes of things to look at later.
- Repeat at step 1.
Now the problem is that these problems will continue and the technical debt will continue to pile up to a level that you can’t do anything without massive re-work, or re-architecting.
Now all of this could have been avoided, if right after step 6, we added a step 6.5 – “Developer creates user stories and tags them as ‘technical debt’.”
The idea here is this, Agile development was built on the principal that we assume that requirements are going to change, and build towards that reality. To that same end, we should assume that we aren’t going to be able to do everything we plan, so let’s plan for, and build in a process to ensure that we can get back to that technical debt wherever possible. If we had our developer go back in, and log 2-3 backlog items, tagged as “technical debt” it would accomplish a couple of things:
- It allows us to keep an eye on how our technical debt is growing.
- Allows us to address it in a manageable fashion.
- Allows for transparency to management and the business.
Tip #2 – Deal with it a little at a time
This should surprise no one, based on the item above. I recommend that we focus on making sure a few technical debt items make their way into every sprint. That way we can tackle these items in a way that ensures that these items don’t pile up on you for later.
Pick out your main sprint based on backlog priority, and set aside a certain amount of time in the sprint, and fill in with technical debt. This approach would say, if I have 3 people in a team, and 40 hours a week, that means I have 280 hours per sprint. So if I said, 210 hours of that sprint is on new feature work, and then 30 hours (10 hours per person) is on technical debt. I could continue to pull down the technical debt to a manageable level.
And there are two schools of thought here on how to pick those items:
Option A – Take as many small items as you can to fill those 30 hours, so if I have small 1-2 hour jobs, I can get more of them in a sprint.
Option B – Sort the list in descending order, and take the largest debt items first. If I have one that costs 10 hours, 8 hours, and 2 hours, those should be the first 3 I take on.
Personally, I find that Option B, tends to work better on the teams I’ve worked on. But you’re mileage might vary. I find that those little stories, tend to cause a vicious cycle of the following:
- Developer gets 2 feature stories, and 5 technical debt stories.
- Developer designs features, and reviews technical debt items.
- Developer works on feature, and pushes off technical debt stories because they are little, and can be done at the end.
- Developer gets caught up in a changing deadlines, and must push off something, technical debt stories get bumped.
- Now not only do the 5 small stories get put back on the pile, but any additional stories are now added.
In my experience giving less stories, and having them be more impactful tends to motivate them getting addressed. But again, your mileage might vary. I would work with your teams on a strategy.
One way to make this fun I found was to make a game out of it, similar to estimation poker. We had a “Debt Wheel”, and each sprint, one developer spun the wheel, and whom ever it landed on, had to pick a big technical debt item to work on this sprint. And then at the end of the sprint, if they finished it, they got to spin the wheel (with their name off it) to see who got the next item. If they didn’t finish it, they had to keep it into the next sprint.
Tip #3 – Use things like nuget packages
I wrote a really in-depth blog post on this, found here. And in it I explain at length how private nuget feeds can help manage technical debt. So I’m going to direct you to that post.
Tip #4 – Single Responsibility
A class (or service) should have only one principle. And should have only one job. The intention here being that it makes it easier to take these pieces out and swap them for something else later. The idea here being that you should focus on building smaller services that can be pulled out and replaced as needed.
Along these same lines, communication between services is another key to keeping technical debt under control, and the idea here is to make sure that there is an abstraction between each service. For example, if I have a service that is supposed to send a request for processing to another service.
One option would be to enable http calls directly between services, but honestly this causes quite a few issues:
- Not Fault Tolerant
- Changes between services aren’t backwards compatible .
- Harder to track and monitor.
But ultimately, if we used a messaging layer, like Service Bus or Kafka, we could eliminate those issues and make our application services into more a black box where each piece functions independent of the others.
Tip #5 – Leverage dependency injection
I have to tell you, I’ve seen a disturbing trend lately where people are saying, “We use micro-services, so we don’t have to use DI.” And I honestly don’t understand this, DI makes it very easy to clean up your code and enforce a plug-and-play architecture that can have classes swapped out at any given moment.
The idea is this, even within a microservice smaller classes can be updated easier. If you are using nuget packages, and the other patterns above, DI is something very easy to implement that has massive impacts on how you build your code making it easier to make changes later, which in turn make it easier to remove the technical debt later.
If you ignore debt it will only get worse!
That right there is the number one thing I hope you take away from this post. All too often in my career have I seen technical debt be the reason that applications are rewritten, or completely rebuilt from scratch, which I’m convinced is a massive risk and rarely works out.