The term Technical Debt gets thrown around a lot in the software industry. If you do not know first-hand what technical debt is, then you should consider yourself lucky. I decided to write this blog post to explain technical debt - my definition, what causes it, and how to improve on it.
This is the first part in a series of posts regarding technical debt. At a later time I will post examples of each type discussed in this article.
The Different Types of Technical Debt
It is important to know the types of technical debt which can be building within a codebase. Since this is not a metric easily measured, these patterns can often help determine the occurance technical debt. Unfortunately, once these symptoms manifest themselves, it is often too late to directly fix the problem. So, here we go!
Complex Dependency Graph
When an application has a complex, interwoven web of dependencies, it becomes increasingly more difficult to make changes without breaking things in other places of the application.
- Functions which are called directly in many places (I would say more than 5 different times) for slightly different purposes.
- These functions can be thousands of lines long.
- They have many paths of execution.
- Often utilize global state instead of passed parameters.
This tends to appear the most in procedural applications, where functions are first-class citizens. If a single function has tens or hundreds of calls in the codebase, then changing the inputs or outputs of that function becomes increasingly more difficult.
I think there's a dependency or two here. 
Legacy codebases often contain complex dependency graphs. Originally, functions were built for a single purpose. Over time, more developers started utilizing these functions, but implementing small extended functionality. Once it begins to get difficult to manage the inputs and outputs (due to the amount of function calls), developers may start to use global flags to dictate special behaviors. The result is a function that is thousands of lines long, and has many different paths of execution.
The debt of complex dependency graphs usually comes in as bugs. If a function is called in many different places with many different parameters (especially global state), it becomes hard to test with automation. Often, your users end up finding your bugs, and developers have to spend more time fixing them.
Developers will also have to work around the dependency graph, essentially throwing rocks inside of a glass house. Attempting to fix bugs in a way that doesn't break a complex dependency graph will take longer, and the solution likely will not be suboptimal.
This also often causes developers to introduce new bugs while fixing old bugs. Again, this is because large monolithic functions with many objects depending on them are difficult - if not impossible - to test.
Short functions which have a single responsibility. If possible, use inheritance to extend existing methods instead of editing them. If many similar but slightly different behaviors are required, consider using a Strategy Pattern to choose behaviors at runtime. This keeps the classes short but separate. Inheritance can be used to share behavior between strategies.
By utilizing these practices, the need for long complex functions will dissapear. Though an object using strategies may still have many paths of execution, it will be easier to test them since each strategy will be shorter and separated.
The issue of parameters and global state may still be a problem. If a function is called many times, it becomes difficult to maintain the inputs and outputs. It may be smart to use a Parameter Object instead of a bunch of globals. The Parameter Object is is an object to hold groups of similar parameters. In the future, you can still add new parameters, and not worry about the function signature changing.
Behaviors That Are Hard To Trace
A user calls in with a problem. This problem is easily replicated, but takes hours to locate the cause.
- UI elements are driven from many places in a codebase.
- There is no single repository for querying data - business logic, database queries and presentation logic are heavily intermixed.
- Frequently using print statement debugging to locate the issue (or shotgun debugging as I call it).
- Presentation logic is duplicated instead of reused.
More than likely, the cause of this code smell is from the beginning stages of the project. The ability to easily trace an issue is directly related to the organization of the project. If a codebase is poorly organized, it is often not immediately obvious where an error is occuring. In a codebase where the presentation logic, business logic, and database queries are properly segregated, debugging for problems becomes trivial; it is easy to see where the issue is failing. When logic is combined together in many places, problems can often be hidden deep in code.
Files with many lines of code become monolithic and difficult to navigate.
This is an easy one: the debt here is that it's going to take longer to fix bugs. If a developer has to spend multiple hours on average finding the causes simple bugs, this is going to add up. Over time, a lower ratio of your development hours will go towards feature development, and more will go to maintaining code. Your issue resolution rate won't increase, however.
Also simple: separate your different types of logic and abstract them from eachother. Your business layer (which manipulates data for user output or processes user input) shouldn't be aware of the database queries needed to store and retrieve data. Your presentation logic shouldn't be aware of the business processes behind a button click. Once these get mixed up, it becomes difficult to figure out where an error occurs.
Patching Instead Of Fixing
- Patching: A solution which covers up, but does not fix the initial problem.
- Fixing: A solution which fixes the initial problem.
In my opinion, this one is a little harder to detect. It can manifest itself in many ways, but you can also draw false positives - that is, it can be hard to discern patching and fixing.
- Bugs will be fixed and often resurface in other places.
- Instead of fixing overly complex or heavily depended-on code, developers often choose to fix the results of that code instead.
The root cause of this issue is actually the combination of the two issues previously discussed. When code is both heavily intermixed (database, business, and presentation logic) and complex dependencies exist, it becomes easier and more economical to patch an issue rather than fix it at the root. In some cases, issues may require a complete rewrite of a great deal of code. In a codebase where it's difficult to perform automated tests, a rewrite tends to be too time-consuming for both developers and quality assurance engineers.
Although patchwork saves time initially, it will slowly dirty the codebase as more and more edge cases are found. Bugs will often be solved in one place, but will reappear somewhere else. Later on, a user may discover a case which the solution did not cover. This must now be fixed in all places previously patched. This process can be harmful to a business as time moves on.
Fix issues at the root of the problem. Though a rewrite of the portion of code is expensive in developer hours, it will most likely prevent more issues from occuring in the future. If the time for a rewrite is not feasible, be sure to note the code as problematic and attempt to hack away at it in the future. A good time is during feature development - if the feature utilizes this code, it may be a good time to modify it.
This one is easy.
- If you find yourself
greping your codebase to fix issues in multiple places.
The cause is sometimes due to poor organization and badly separated code. Most of the time, however, the cause is poor development practices. Sometimes it is just easier to rip a piece of code out of
ModuleA and throw it in
The debt here is two-fold. When an issue is found in code that has been duplicated, it must be fixed in all places it occurs. There also runs the risk that it will not get fixed everywhere. The second debt introduced occurs when code is copied and pasted that contains an issue. You are increasing the bug count of your code base.
Modularize. If that's a word. The first time you are tempted to hit Control+C, consider the implications. If more than a few lines are being copied, and if those lines contain any business logic (manipulation of data or user input), then it belongs encapsulated in its own module of some sort. Business logic is likely to change in the future.
Listed above are just of the few types of technical debt I've experienced in active codebases. They tend to exhibit themselves more as a codebase grows.
In the next part of this series, I'll look at the solutions to technical debt in general.