In last week's post we defined technical debt and covered some examples of technical debt in the wild. According to Appian, 58% of organizations agreed that managing technical debt hindered the average software developer's ability to develop applications their users wanted.
In this post I want to explore examples of tech debt in existing code in more detail. Ideally, having a good understanding of the various types of technical debt and what they look like can help you avoid creating more of it than you need to.
Below are examples of technical debt categorized by where they appear.
There are many ways to create technical debt and types of technical debt.
As a quick and dirty reminder, tech debt is a debt metaphor popularized by Ward Cunningham, who defines technical debt thusly: “With borrowed money, you can do something sooner than you might otherwise, but then until you pay back that money you’ll be paying interest. I thought borrowing money was a good idea, I thought that rushing software out the door to get some experience with it was a good idea, but that of course, you would eventually go back and as you learned things about that software you would repay that loan by refactoring the program to reflect your experience as you acquired it.” Later, Steve McConnell elaborated on the metaphor expanding technical debt into two types: intentional and unintentional.
One source of technical debt is breaking the rules of “clean code.” (For more on this topic, check out Exploring Scrum: The Fundamentals) These rules include, but are not limited to the “once and only once” rule. Sometimes you write bad code that isn’t modular, code whose modules aren’t cohesive, code whose modules are named weirdly and not according to what they’re supposed to do, or code that isn’t readable. Sometimes you use uncommon coding practices that aren't well-known. Sometimes you don’t name your variables, methods, and classes based on what they're supposed to do.
Sometimes you write “ugly” code, or code with “long methods, lots of temp variables, parameter lists that get passed to multiple functions, and methods that know more than they need to about the data model.” Senior Full Stack Web Engineer Curtis Autery writes.
Evaldas, Technical Team Lead at Euromonitor International, wrote that his current codebase is an example of technical debt. The databases contain a decade-plus-old “huge relational monolith.”
Another example of technical debt in the code comes from Ipek Ozkaya, Senior Member of Technical Staff at Carnegie Mellon Software Engineering Institute. One successful maritime equipment company managed to amass 3 million lines of code over 16 years, creating an astonishing amount of technical debt. Over the years new companies entered the marketplace, staff turned over, and the company launched more and more products that would end up under warranty or maintenance contracts.
Technical debt in the software system slowed the team down considerably and created massive amounts of work. It was difficult to make small changes or additions to the products and doing so often broke the code, creating short-term and long-term difficulties. When they launched new products, every release required developers to do several days of manual and labor-intensive regression testing with existing products.
One of the biggest sources of technical debt is insufficient or out-of-date documentation. Ideally, developers will document why they made their choices and what they intended when they made them, how they grouped their entities into modules (and why), etc. It’s also a best practice to update your documentation as you refactor and add to your code.
Usually, technical debt in the documentation comes from coders simply moving too fast and taking shortcuts. But there are certain edge cases to look out for. For example, Programmer and CS Professor Chelsea Troy noted one “egregious case” of perfectionism that led to technical debt in the documentation. In this case, Troy took over a project from a developer who had merged all her own pull requests before leaving the organization. No one knew anything about her project, leaving Troy to rebuild the project context from scratch at a cost of about $30,000.
To keep this from happening again, the director asked a developer who they suspected might have reviewed the departed programmer’s code to review Troy’s code. “This developer spoke pejoratively of my predecessor’s work and skill, which clued me into their working relationship,” Troy wrote. “When the developer started reviewing my work, his comments focused exclusively on choices he did not agree with. Wow—no wonder the original developer merged all her own PRs.”
Unsurprisingly, the maritime equipment company also had tech debt in the documentation. New hires generally had no idea why the code would break because the documentation for design and program choices was sparse and outdated.
Robert Lefkowitz is the former Chief Architect of Warby Parker. He gave a talk in 2019 where he described how project management decisions can impact tech debt. For example, design debt is often introduced through third-party libraries, rather than just within the code itself. “If you don’t want tech debt, avoid using libraries or frameworks,” Lefkowitz said. His example was React.js. Designed by Facebook, the library is great for a complicated web and mobile front end used by a billion people. But most companies aren’t working at that scale, and so using React.js would slow development.
To demonstrate how much tech debt external libraries can add, Lefkowitz removed all the dependencies from a sample iOS application, including external libraries such as Alamofire. This took the code from 80,000 lines to 35,000 lines. Developers could replace the functionality by sourcing to iOS since it already understands HTTP, or recode it from scratch.
The move resulted in 45,000 fewer lines of code to maintain. Even if you’d imported the code, when the owner updates it you’d have to update it in the programs that use it. To further quantify the gain, Lefkowitz pointed out that if an application relies on 300 dependencies, and developers update each once a year, then developers will have to rebuild the application nearly every day. Taking this into account, it might make more sense to reinvent the wheel unless you’re building projects with at least 5 million lines of code.
Engineering Manager Noah Thorp put it this way, “We pay rent on every line of code, every day.” And developers pay rent on their dependencies as well. These can include libraries and frameworks as well as all the services connected to your applications, which also generally have a library connected. “When you add a library to your project, you are paying rent on that entire library and your use of it,” Nerd Coach Carl Tashian wrote. “So, you need to make a good case for every library, every plugin. Even the tiny ones. They add up fast. And if you take the ultralight, disciplined approach, you will be amazed at how quickly you can move.”
Assuming you needed features from Alamofire and now need to build them yourself, removing Alamofire, for example, will probably lead to more tech debt. At Clockwise, we use React.js because we’re large enough that we benefit from sharing code in modules/components that we can reuse across our codebase, which usually helps us avoid tech debt.
Bottom line: Avoiding using libraries or frameworks altogether isn't good advice, but it’s smart to make the case for each dependency you add.
Another example of tech debt outside the code itself comes from Redbubble’s Director of Engineering Tom Sommer. They’d custom-built their Kubernetes cluster before now-popular more managed solutions like EKS, GKE existed. So before they could switch they needed to consider how it might impact their applications and team.
Technical debt lives in the code itself, the documentation, and in your libraries and frameworks. Hopefully these examples of technical debt help you better understand what it is, how it works, and how to minimize it going forward.