As a company that specializes in working with legacy code, we’ve run into our fair share of technical debt over the years. For those who may be new to the term, technical debt is a metaphor that was developed by Ward Cunningham to describe how certain development activities can make your codebase more difficult to work with, and this is the quote that gets cited most often when introducing the concept:
Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite… The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation. -Ward Cunningham
We’ve seen this happen — teams that have deferred their maintenance for so long that any tiny change to the code takes forever to get out the door. Improving that not-quite-right-code is a big part of paying down technical debt.
But there are some other things that contribute to technical debt that you won’t find in the codebase. In a follow-up video to his original post about technical debt, Cunningham described that it’s not just the code that contributes to technical debt, it’s also about a misalignment of understanding.
If we failed to make our program align with what we then understood to be the proper way to think about our financial objects, then we were going to continually stumble over that disagreement and that would slow us down which was like paying interest on a loan. -Ward Cunningham
What contributes to this misalignment and disagreement? Based on our work with clients over the past decade, we’ve observed two distinct areas outside of the code itself that contribute significantly to the amount of technical debt in a system.
- Lack of healthy communication
- An organizational structure and culture that hinders progress
Looking at all of this together, we can start to see that it’s not just what’s in the code that contributes to technical debt, there are actually three different areas that you have to pay attention to if you want to start tackling this complex problem.
- Code debt
- Communication debt
- Organization debt
Let’s explore each of these in more detail and see how we can align our entire system to supercharge our development efforts.
When people think about technical debt, the natural first place to consider is the code itself. When code is hard to change and deploy, you’re not maximizing your productivity. In an ideal world, a developer would have almost no friction in their process of understanding the code, making a change, testing the change, and deploying the change. Let’s look at some common places within this process where code debt is likely to occur.
Cyclomatic complexity measures the number of unique paths through code. The more paths a chunk of code has, the more difficult it is for a human to interpret and the more tests are required if a change needs to be made. In general, for methods or functions, a complexity score of 4 is good, between 5 and 7 is complex, between 8 and 10 is high, and above that is extremely complex.
At Corgibytes, we’ve seen cyclomatic complexity scores of over 1,500 in a single method (true story) — and at that level it’s incredibly difficult to maintain the mental model of something that complex. Like a tangled knot, complexity can be reduced through practices like refactoring, but doing so often takes time and patience. This means that complex code is more expensive to maintain and developers’ morale goes down because their productivity is so severely impacted.
Lack of Automated Tests
In his book Working Effectively With Legacy Code, Michael Feathers famously described technical debt as “code without tests.” Automated tests are incredibly important to keeping technical debt at bay. When the tests are part of the code (and created first as part of the development process) you get context into expectations, can more easily fix bugs, are able to deploy changes with confidence that you won’t break the build, and many more benefits. A good automated test suite is a fundamental safety net of any technical debt paydown initiative.
Of course, there are lots of other places within the code that help make development easier. Using continuous integration and continuous deployment, taking advantage of containerization, and properly managing dependencies are just a few examples.
Years ago, writing out a big long list of requirements in a document before development took place was the norm. Then, smart people realized that since software is a complex system filled with interdependencies, developing software in this “waterfall” way led to severe cost overruns, project delays, and deflated developer morale. And, while many truly useful practices have emerged to help correct the challenges of a waterfall approach, some people have misinterpreted the message to mean “The code is the only documentation you should ever need — ever. So don’t waste your time writing anything down if it’s not going to be compiled.”
There’s an important nuance here that’s getting tossed out now that the pendulum has swung to the other side. When all types of communication artifacts are conflated with the burdensome requirement novels that were typical of the past, it conditions developers to omit important information from a codebase — most notably rationale. When we can’t reconstruct why decisions were made, we’re limiting our understanding about what the code does, and that can severely slow down development.
When we review codebases to understand the level of technical debt we’re dealing with, important communication clues live in places such as:
- Commit messages (with detailed descriptions!)
- Pull/Merge Request comments
- README files
- Wikis and other internal documentation
And that just scratches the surface. When I’ve done workshops to help development teams identify the different types of communication artifacts that are generated as a result of day-to-day development, the list quickly becomes eye opening and more comprehensive than people realize. Here’s one look at how you can discover and categorize all the areas where natural language is useful in sustaining a software system.
Healthy code requires healthy communication. And healthy communication doesn’t have to take the form of novel-like specification documents. No. Instead, think about ways to make your documentation executable, or as close to the code as possible, by using tools that make this easy.
It’s also useful to pay attention to behaviors that preserve intent. You can have the best goals and tooling in the world, but if you and your team don’t have good practices and habits for capturing your ideas as you work, you’ll struggle to make traction.
As you work through the code, take a moment to think about how easy your work is to read or understand without context. Taking the time now for writing quality commit messages can save you (or someone else) hours of work in the future. It’s also worth the effort to practice good code hygiene. Just like how brushing your teeth every day contributes to your overall health, getting into the healthy habit of making your code easy-to-read can pay off in dividends when trying to stave off technical debt. Practices such as adhering to consistent white space rules, clearly naming methods, functions and variables, deleting commenting out code, turning magic numbers into constants, etc. are the veritable teeth-brushing of crafting quality code. Other examples include Llewellyn Falco, who includes writing documentation in his definition of done and Arlo Belshee has come up with a nifty notation to help make it easy to categorize changes. There are lots of different opportunities to preserve the rationale and decision making that went into writing software.
Another place communication debt can accrue is in misunderstanding between business and engineering teams. One example might be that to someone who doesn’t have their hands in the code every day, the words “dependents” and “dependencies” might be roughly synonymous, but to engineering teams, these terms are complete opposites. Getting into syntactic alignment within the domain of a project is incredibly challenging and important. To enhance domain clarity, practices such as Domain-Driven Design and Behavior-Driven Development emphasize the use of a “ubiquitous language” between the engineering and business teams so that terms have the same meaning for everyone. This alignment doesn’t appear by accident. Good communication on a team requires commitment and discipline.
In our experience, the productivity difference between the clients that prioritize good communication and those that don’t is significant. When communication flows well throughout an organization, we’re able to onboard, read the code, get up to speed on the domain, spot problems, analyze solutions, and ultimately deliver value much more quickly.
Panning out to the 10,000 foot view, there’s another area to consider — the corporate infrastructure and cultural norms of the organization as a whole. Typically, bureaucracies that have a command-and-control org chart along with a culture of secrecy and silos are rife with technical debt, too. This doesn’t mean that individual teams can’t be successful within such an entity. We’ve certainly observed high-functioning groups within organizations that aren’t aligned with the structure and culture that keep technical debt at bay, although these teams are rare. If you’re looking to get the most out of managing technical debt, you have to include the organization as a whole in your consideration set.
A couple of months ago, August Lilleaas wrote a nice summary of a paper that Microsoft published about the link between organizational structure and post-launch defects (bugs). Microsoft’s research determined that “organizational metrics…were statistically significant predictors of failure-proneness.” These organizational metrics consisted of things like the number of engineers on a project, the percentage of the organization that contributes to a project’s development, how many people can make decisions or have positional authority, etc. As part of the research, Microsoft built a model that measured how organizational structure compared to established predictive models (such as churn, code complexity, dependency analysis, etc.) for post-launch failures. The results? Organizational structure emerged as the most accurate predictor and these findings have been replicated by other research teams.
Conway’s Law is another oft-cited principle that has demonstrated through research how the architecture of a codebase is bound to be a mirror image of the communication structures that are set up by the organization that builds it. In general, structures where membership is closed, authority is formal and hierarchical, and the work is planned and coordinated will lead to monolithic architectures that are difficult to change. In contrast, decentralized, informal, and open systems that rely on emergent design principles are more likely to yield modular architectures that are easier to work with. It’s not just individuals that contribute to technical debt, it’s the design of the organization itself. As W. Edwards Deming famously said, “A bad system will beat a good person every time.”
Our last stop on our robust technical debt journey has to do with the culture of an organization. While organizational infrastructure looks at positions, titles, and power dynamics, culture is about shared behavioral norms. In 2016, Google released the findings of Project Aristotle, a quest to deeply understand what makes a team effective. A surprising result surfaced to the top — psychological safety, which is the ability for a team member to share their ideas and opinions and take risks without fear of being shamed or embarrassed. This means that empathy, trust, vulnerability, and all the “squishy people stuff” also has a lot to do with your technical debt.
For example, if your culture doesn’t support interpersonal risk taking, a team member with valuable insight may hold back and not share even if they’ve spotted a flaw that could have saved the team hours of collective work. Or, if trust is low, back channeling and political jockeying may take the place of productive discussions. This is a topic that is near and dear to my heart and if you’re interested in diving in deeper, I recently released a course on LinkedIn Learning titled Creating An Agile Culture that covers what healthy cultural alignment looks like.
So, Now What?
My hope is that if you’ve made it down this far into the article you’re more inspired than overwhelmed. Technical debt is a complex challenge that incorporates the entire organization. If you’re a developer with your hands in the code every day, take advantage of small opportunities to keep your code legible and preserve your rationale. If your role is more managerial or strategic, paying attention to culture and organizational systems might be your highest contribution. And of course, everyone should be working on communicating effectively and aligning their understanding with others. Technical debt doesn’t accrue in a vacuum, and it won’t be paid down that way either. Paying attention to code debt, communication debt, and organizational debt holistically is what the best teams do to keep their code easy to maintain and a joy to work with.
What are your thoughts? Have you observed how communication and organizational debt have impacted a codebase? Are there other aspects beyond these that you think are also important to pay attention to? Share your ideas in the comments below and let’s keep the conversation going.