All teams struggle with technical debt, and every software team creates technical debt that it strives to minimize. Some teams are better at repaying this debt than others and succeed in recognizing tech debt’s negative impacts, quantifying possible impacts, prioritizing fixes, and committing to a resolution plan.
Technical debt drags down software development. It makes code harder to build, run, and deploy and may even cause major production outages. Addressing technical issues is the easy part and usually entails refactoring or changing some code. The hard part is prioritizing and quantifying technical debt. Software teams fail to resolve debt because they don’t get the ball rolling soon enough, not because they can’t fix technical issues.
Repaying technical debt and developing features constantly compete for backlog priority. When transitioning application development from Waterfall to Agile, technical debt is often inherited from old systems and code. There’s no one-size-fits-all solution for eliminating tech debt, but it’s clear that agile teams must routinely tackle it in many different forms. And working on the wrong issues for a particular type of debt wastes precious resources and time, defeating agile principles.
Three Faces: Local, Global, and Systemic
Some debts eventually force teams to declare software bankruptcy, while others warrant simple interest payments. Trivial debts can be ignored since they may be repaid at any point. Teams fall into the fallacy that all technical debt should be eliminated, but the real-world truth is that only the most untenable debt needs to be discarded. Technical debt broadly falls into three categories: local, global, and systemic. And it is important to properly categorize each type to separate the tenable from the untenable.
is self-contained in a single method, class, or function. Imagine SOLID violations applied to a method, class, function, or single file. Their impacts are only felt when modifying the code in question.
is self-contained in a single application or service. Unlike local debt, global debt involves SOLID violations across subsystems in a larger system that spread to seemingly unrelated areas. Examples include a mismatch between abstraction layers (presentation vs. data), problems at other integration points, and hard coupling. These issues don’t impact consumers of the application or service but are felt when modifying the service itself.
spills across multiple applications, infrastructures, or even teams. Examples include multiple services sharing a data store, high coupling across different bounded contexts, and a stark mismatch between technical implementations and business logic. Systemic debts are untenable in the long term.
Each category of debt carries different impacts, so they need to be prioritized and handled differently.
Local Technical Debt: Agile Low-Hanging Fruit
Known refactoring techniques like “extract method” or “extract superclass” typically solve local technical debt. They may even mirror known code katas, which makes them the least consequential or even downright trivial. Let’s take a look at an example:
An engineer wrote code a year ago. The code is overcomplicated but covered by tests. It’s not the best code by any stretch, but it’s functional. The business requirements haven’t changed, so the code stays as it is.
Engineers may want to refactor the code, but it’s not worth the effort since the code hasn’t changed in a year. On the other hand, local tech debt in frequently modified code inhibits development, as such code is usually at the core of a business and more developers must understand and modify it. Eliminating debt here is like patching up potholes to maintain a highway’s fast lane. If tech debt is not properly addressed, then engineers will write increasingly brittle code as a workaround, which may eventually create global technical debt.
Static code analysis tools can mitigate local technical debt, and running them before commits and as part of the deployment pipeline virtually eliminates it. But you’ll need a different approach to combat existing problems. To start with, it’s good to assign local fixes to new team members as a learning exercise. Teams can also apply the “boy scout rule” that the code should be left better than it was before, a practice that works well since code improvements are proportional to how often the code is modified.
Global Tech Debt: First Signs of Trouble
Global tech debt is harmful. Here’s an example from a typical MVC web application:
The team isolates complex view layer concerns into a presentation layer, and a requirement comes in that the UI needs to display custom data. The existing middle layers don’t support this, so the presentation layer calls directly to the database. This is first a leaky abstraction, which may trigger the broken window theory and devolve other areas.
This appears in the wild more often than engineers like to admit. Unfortunately, there’s no quick fix for these issues, as a solution requires focus on all related code. Senior engineers are best suited to repay the debt by carefully separating concerns, creating boundaries, and enforcing bigger architectural concerns.
Judicious governance and workflow improvements can mitigate issues before they appear, and adding careful code review to specific releases is a good starting place. Not all releases are created equal. Releases that change code across boundaries or rework core business concepts must be vetted carefully to minimize technical debt. Releases that modify a single less-used class do not. These concerns should shift left in the workflow, meaning that engineers should discuss potential technical debt as early in the process as possible in order to mitigate problems later on.
Global tech debt must be attended to when it inhibits the development of frequently modified code. If the solution is not immediately clear, then spike on a potential 80/20 solution and take action swiftly before the situation devolves into a systemic issue. Clean up will take time out from sprints, so plan accordingly.
Systemic Tech Debt
Systemic tech debt is the most dangerous form, manifesting itself throughout all SDLC phases. It can seriously slow down development, demotivate engineers, force a ground-up rewrite, or even kill the company. Systemic issues are this powerful because they capture problems at the architectural level. Issues that arise include using a NoSQL database where an RDBMS is the correct choice, creating tight coupling between independent services, communicating across bounded contexts via a shared database, splitting business concerns into too many microservices, and choosing the wrong deployment architecture.
Systemic issues tend to sneak up on development teams. They are always there, lurking just under the surface until some innocuous change or request creates a forehead-smacking moment. Then everyone in the room sighs, realizing that something is really wrong.
There’s no prescriptive solution to resolving systemic issues. Repaying this debt takes teams of engineers, careful planning and execution, and time—the scarcest resource. It’s therefore in the organization’s best interest to combat these problems earlier rather than later.
Building evolvable architecture is an automated way to combat systemic issues. Building Evolutionary Architectures proposes testing key architectural characteristics such as component coupling or durability with fitness functions. The deployment pipeline executes the fitness functions as part of its criteria verification, thus rejecting severely problematic code. If this approach is not possible, then you’ll have to implement strict high-level code reviews with senior engineering staff to identify issues manually.
Now, at this point, it’s possible to prioritize and craft a strategy to mitigate your organization’s technical debt.
Prioritizing the Backlog
Technical debt becomes a problem when it’s not attended to. Many organizations ignore the issue, hoping that the problem will resolve itself, or focus their efforts in the wrong direction. Teams also make a mistake in accounting for technical debt across multiple projects and the direct as well as indirect impacts it can have. For example, tech debt in dependent projects can inhibit delivery across other projects. Agile teams must understand each debt’s worth is a factor of its scope (local, global, systemic), impact on frequently modified code, and time required to fix it. Teams must prioritize fixes and coordinate releases across all project backlogs.
Your tech debt strategy should be an 80/20 approach for improving development on the most frequently modified code paths. The goal isn’t to eliminate technical debt but to keep it easily manageable and incorporate it into your agile delivery. Also, if it’s not inhibiting common development activities, then let it be. This keeps the team working in the right direction.
So addressing inconsequential local debt can be moved to the bottom of the pile, while addressing global tech debt is likely the biggest 80/20 win since it hedges against systemic debt and improves development of frequently modified code. Prioritize spikes aimed at 80/20 solutions and go from there. This leaves you with the issue of systemic technical debts. These may take many sprints to resolve and thus must be part of a long-term strategy. Again, prioritize spikes at 80/20 solutions for systemic problems. The spike result should help estimate fixes and prioritize them against other items in the backlog.
Do not aim to wipe out technical debt in a single massive release. Fixes for technical debt should be released in continuous small batches. Practically speaking, these releases can in fact bundle multiple local tech-debt fixes. Global and systemic fixes require multiple releases that are small and focused. Addressing global and systemic debt is complicated, so move slowly and deliberately and avoid including any other changes in releases for these forms of debt. Your release management strategy should also promote visibility into fixes and their relationship with other projects and releases as well.
Prioritizing the Future via Technical Investment
Technical investments in automated testing, static analysis, and fitness functions bake quality into the process. Over time, this moves tech debt out of individual backlog tickets and into the current culture. And this kind of agile culture change can mitigate problems to the point where systemic debt rarely appears.
This future likely feels far away, so you’ll need to start with prioritizing the backlog by considering the different tech-debt categories and the affected code and business flows. You’ll also need to prioritize efforts across all projects since not all projects are created equally. Achieving visibility into all projects and releases with software such as Panaya is a key factor in deciding what to tackle.
Eschew fixing local issues and prioritize integrating static code analysis into the existing test suite to mitigate the creation of more local technical debt. Next, prioritize spikes aimed at 80/20 solutions for the worst global tech-debt offenders. Don’t bite off more than you can chew in the beginning by tackling systemic debts. Craft a technical strategy for mitigating their impacts and focus on 80/20 spikes after addressing pressing pain points.