Attacking Technical Debt
This isn't the boogeyman. This is real.
And it's everywhere.
You can try to hide it in the closet so that your visitors never know; but, just like in the Tell-Tale Heart, you'll know that it's there. With every beat of the heart -- every code modification, build, or deployment -- it'll remind you that you can't escape it.
Unless you do something about it.
Why It Matters
So what's the big deal, anyway?
We know it's not ideal right now, but we'll get to it later.
And when, exactly, is later? After you wrap up that release? Fix those few nagging bugs? When Jessica comes back from vacation because she's our best, most experienced developer?
When that release gets done, those bugs are wrapped up, and Jessica is back in the office, there will be: more releases, more bugs, and more people on vacation. All of this added on top of the existing technical debt that you couldn't address earlier.
And while the technical debt spiral continues, frustration may begin to mount not just with the developers, but also the end users. Does an end user really care that the javascript for the button that makes the widget work is in one giant file, loaded above the fold, and is intertwined with dependencies?
Probably not.
But an end user does care when they ask for an enhancement to said button and the development team comes back with Well, that'll take a little longer than it did last time. The code for that button is pretty complex and only Jason understands it and he's tied up with other things.
Technical debt affects end users and developers alike, albeit differently. When you want to build and enhance software quickly and with quality, the mounting technical debt becomes the elephant in the code that will never forget the untended TODOs, the dubious architectural decisions, and the Eh, we'll get to it eventually mindset.
Identifying It
All code bases are different, but they have a few things in common:
- They usually have more than one person maintaining it.
- They evolve over a period of time.
- They might have some "get it done" code (read: hacky) in there to put out a fire that needed to be solved now.
- They may have portions of undocumented/uncommented functionality.
- Oh... those TODOs...
The evolution of the code base is natural and, depending on the level of rigor the development team's process allows, technical debt may accumulate more slowly. For all intents and purposes, it's pretty difficult to completely prevent its accumulation (although, it can be mitigated... more on that below).
Now, it's easy to jump into some code that you've never seen before and are unfamiliar with, settle in to a review session, throw up your hands half way through, and say There's so much crazy here that the whole thing needs to be rewritten! Does that mean the code is terrible? Faulty? Needs to be rewritten from the ground up?
Not necessarily.
One thing in reviewing code is really reading it and understanding the context. If there's one thing that any developer knows is that there are a million different ways to approach and solve a problem. It's also much easier to adopt your own view as to how you see and solve problems and write new code that achieves the same goal than reading and understanding something that may have been written by a handful of people over a couple of years.
That's where context comes into play. Does the code work well? Does it perform as expected? Is anyone still here that worked on it previously (if you can figure that out)? For example, if you're looking at a block of code that has been through the rigors of a few years of testing, bug fixes, and production releases, you know that it's been battle-tested and is hopefully doing its intended function. Should we rewrite it from scratch? Probably not. Is there an opportunity to refine the style or make it more performant (dubious string allocations I'm looking at you)? Maybe.
The point is not all janky written code is technical debt. With some context, there may be a (hopefully good) reason as to why a particular class breaks the SOLID principle. However, there are some general things to look for when reviewing code to spot technical debt that needs to be taken care of:
- Addressing poor performance (based on empirical evidence, not premature optimization).
- Eliminating dead code.
- Addressing security concerns, memory leaks, etc.
- Modifying code to match patterns used throughout the rest of the application (where and when appropriate).
- Decomposing that single method that's over a 2000 lines.
Those are just a few examples, but if you take the time to read and understand code with the proper context, you'll be able to identify code smells with experience.
(As a brief side note, we know that new frameworks come and go almost every day. Is it really technical debt that everything was written on framework A instead of the newly minted framework B? In my opinion, not really, but that depends on a number of things: the skill set of the team, support/maturity of framework A over framework B, and the future plans of the product (just to name a few). Use your judgement, but never jump to a brand new framework without carefully weighing the pros and cons. Besides, by the time you've finished your implementation and are ready for the roll out, 3 more frameworks would've popped up.)
Attacking It
You've gone through the code, met with the team, agreed that Yeah, we should probably fix that.
So what's next?
Commit to fixing it.
If you don't commit to fixing -- attacking -- technical debt, it will continue pile up over time until you are either forced to deal with it or someone flips a table and convinces everyone that it needs to be completely rewritten from scratch.
So, let's rewrite it (coding montage):
Ok, that's done. What now? How long did that take? Is anything else going to change to prevent debt from piling up again? No?
Fast forward an indeterminate amount of time and someone's flipping a table again. That person begins to convince everyone that everything needs to be rewritten from scratch... again.
So, let's rewrite it again (another coding montage):
Whew, two rewrites in [insert your period of time here]. Did anything change? No? Table flip and rewrite?...
Ok, I won't do that again, but I think I've made my point.
I'm not 100% against complete rewrites, but taking a measured approach to continually address technical debt is important if you ever plan to ease the burden in the future.
Here are a few things you can do to begin attacking technical debt:
1. Commitment
You need the commitment from members of the development team as well as the support of business units to spend time consistently tackling technical debt. Do whatever works best for you and your organization (e.g. setting aside a few hours a week, month, etc.), just make it consistent. Just commit.
2. Be Iterative
If there's something large that needs to be broken down, don't feel that you have to bite off the whole thing at once. Break it down, piece by piece and apply changes to address small parts of a problem while testing along the way. This will lower your risk of introducing new problems.
Depending on your branching strategy, it may be a good idea to tackle a large effort in another branch. Just make sure to following good branch management practices.
3. Unit Tests are your Friend
Even if you don't follow test-driven development (TDD), unit tests can be your friend when refactoring and addressing technical debt. The tests can be used as your baseline of functionality to which your newly written code must adhere. That way, you can be more confident that your new code is doing what the old code did. And if your new code needs to do something a bit different, it's okay to write some tests for that, too.
4. Persistence
Review your process as you tackle technical debt. Make tweaks as you go along. Committing to addressing it is one step, but staying persistent with addressing problems and getting better at it will enable you to mitigate the accumulation of future debt.
If you take these actions in battling technical debt, you'll not only improve the quality of your software, but also cultivate happy developers and end users.