What is technical debt?
Technical debt (TD) is the cost of reworking software that is difficult to modify or maintain. It usually happens when the development team takes shortcuts in favor of speed of delivery.
Tech debt is a metaphor and an umbrella term for multiple different types of debt. This makes it tricky to define, pinpoint or track. In this article, we’ll discuss how to prevent the most common types: code debt and architectural debt. And for engineers, the tell-tale signs are 1. A change to the software is unusually hard to implement or 2. The code became more complicated after introducing a simple change.
Full Stack Engineer
From a developer’s point of view, technical debt is anything you tell yourself you’re going to fix “later.” But more often than not, later never comes.
There’s still much confusion and debate about what counts as tech debt. So let’s clear that up. From our perspective, tech debt can’t be boiled down to the number of FIXMEs or TODOs in the codebase – it’s more nuanced than that. It’s also not identical to bugs or badly-written code. Bugs can be caused by tech debt, but TD isn’t limited to bugs. And bad or messy code doesn’t always evolve into tech debt.
Why is it important to reduce tech debt?
Left unchecked, technical debt can have a serious impact on your company’s bottom line.
Like financial debt, TD accrues interest over time. The longer it’s ignored, the more costly it becomes. For example, if an engineering team fails to update dependencies regularly, it can lead to unexpected costs down the road. Why? Because when a library or framework deprecates, the team will have to spend unplanned time repairing or replacing it. Over months and years, unmanaged tech debt erodes the quality and scalability of your product. It can also hinder your ability to hire new devs or rely on documentation.
It’s estimated that software development teams waste about 23-42% of their time dealing with the ramifications of technical debt. This drain on resources typically impacts delivery speed. Timelines often get harder to estimate, product improvements get sidelined, and stakeholders grow uneasy with the lack of progress. Not to mention, developers can quickly get burned out when they are constantly working on issues that could have been avoided.
Types of tech debt
When Wade Cunningham first coined the term technical debt in the 1990s, he was mainly referring to code debt. However, the definition has expanded a lot since then. We can now say there are several different types of tech debt, including:
Architectural debt occurs when we make sub-optimal decisions about the system architecture. For example, you build your web application using a low-code platform (e.g WordPress). But later, when applying user feedback, you realize that your product requires way more customization than the platform can offer.
Code debt. This is the classic definition of tech debt. It refers to the consequences of writing code that is difficult to interpret, update or maintain. The code might be overcomplicated, inconsistent, or contain errors that can result in bugs or other issues.
Infrastructure debt can accumulate when the infrastructure (e.g. servers, databases) supporting a system is not properly maintained or upgraded. This can result in critical performance issues, such as unplanned downtime or the slow loading time of key product features.
Documentation debt occurs when developers fail to document their code correctly. Missing, incomplete, or outdated explanations can cause confusion when coworkers or new team members come in contact with that code. In bigger systems, doc debt can also make it hard for engineers to interpret how certain processes or features actually work, and how parts of the codebase are connected.
Data debt is incurred when software teams don’t properly invest in data management. Failing to break down information silos can cause data duplication or missing data, making it difficult for product teams to properly connect with users.
Security debt builds up when proper security measures are not considered during the software development process. For example, the team delays fixing a known system vulnerability, which increases the likelihood of an SQL injection attack. Unmanaged security debt can lead to major data breaches and other serious violations.
Common tech debt triggers
Business and management causes
- Tight deadlines are one of the most common causes of tech debt. When management imposes strict deadlines for software delivery, engineering teams may be forced to take shortcuts (e.g. building a system that’s functional, but not scalable). Whether these shortcuts are intentional or not, organizations should prepare to deal with the consequences later i.e. allocating development resources for refactoring.
- Poor communication. If engineers aren’t aware of the project’s goals, priorities, and timelines, they won’t be able to make decisions that are right for the product. Lack of discussion around product architecture or infrastructure design can lead to critical system failures later on.
- Lack of planning. Failing to map out the product vision and set achievable goals will almost certainly cause tech debt. When a team lacks direction, they are more likely to choose the wrong approach (e.g inflexible web framework, unreliable hosting provider, etc.) or work on tasks that don’t move the project forward.
- Shifting strategy or requirements. Abrupt changes or additions to the project spec can also lead to an accumulation of tech debt. For most agile development teams, however, this is unavoidable. Constant iteration is a natural part of the process.
- Scarcity of resources. Understaffed development teams usually don’t have the bandwidth to check edge cases. Most of the time, they still have deadlines to contend with, which can force them to sidestep best practices (i.e. not conducting PR reviews, running automated tests, writing documentation, etc.)
Engineering team causes
- Inexperienced engineers often make poor decisions that lead to tech debt. For instance, they are more likely to write duplicate code, making potential bugs harder to find and fix. They can be less likely to follow naming conventions, use proper programming language patterns and structures or properly format their code, making it difficult for other engineers to understand and update it in the future.
- Not striving for excellence. When engineers don’t take ownership of their work or care enough about the quality, then the product suffers. Sloppiness can lead to major problems in the codebase that will have to be addressed at some point.
- Overengineering. On the other hand, aiming for perfection can also backfire. Complexity for the sake of complexity often causes unnecessary roadblocks. For example, the team switches from a monolithic to a microservices architecture too early, which makes it difficult to apply changes during subsequent iterations.
- Mistakes. Everyone makes them – even your senior devs. Sometimes, despite prior research and planning, the right decision doesn’t become clear until after a change has been implemented. This is one of the reasons why tech debt is inevitable.
Intentional vs unintentional tech debt
It’s important to remember that tech debt isn’t always a bad thing. More often than not, it’s an unwelcome burden. Other times, a product actually benefits from taking it on. But regardless of how tech debt is perceived by the team, it can be split into two camps: intentional and unintentional.
- Intentional tech debt: The team deliberately incurs tech debt in order to achieve a short-term goal. Typically, this happens early in the product development process when speed to market is a priority (prototyping, MVP building, etc.) But it can also occur when the team is under pressure to release a new update, feature, or meet any kind of tight deadline.
- Unintentional tech debt: This can occur as a result of poor design choices, lack of attention to detail, lack of product understanding, and other software development pitfalls. In other words, it’s mostly accidental.
Martin Fowler’s ‘Technical Debt Quadrant’ illustrates the difference between deliberate and inadvertent debt and their reckless and prudent counterparts.
How we reduce tech debt at Railsware
As the old adage goes, ‘prevention is better than cure.’ Here are some of the things we do to prevent, track, and tackle tech debt.
Without a product strategy – and a plan on how to execute it – it’s virtually impossible for an engineering team to make the right decisions at every turn. That’s why having a product roadmap is so important.
A roadmap is more than just a list of tasks to be completed. We consider it a living document that displays the actionable items of a product strategy. Features are clearly prioritized, so that team members know where to focus their efforts from the get-go. At a glance, engineers can learn what the project’s main priorities are and grasp how their code informs the bigger picture.
Meanwhile, the product backlog helps teams keep track of tech debt (among other things). Although it’s easier to ignore code smells, there are benefits to properly recording tech debt each time it crops up. The more visible it is, the less chance it has to fester.
So, engineers should create tickets in the backlog each time they spot an inflexibility in the codebase. Then, whenever there are resources to spare, the team can assess the issue’s priority level and start investigating its root cause.
Focusing on what matters
Not all tech debt is created equal. Oftentimes, it doesn’t make sense to refactor a feature that is rarely modified in the first place. Turn your attention to the code that actually matters. Work on strengthening key product architecture and infrastructure elements with each iteration. It’s better to gradually increase the overall agility of your product than constantly tinker with ‘good enough’ code.
At Railsware, we refactor only when there is an urgent and explicit need for it. That way, we don’t sink resources into fixing non-essential things. At the same time, our engineers are encouraged to make minor, incremental improvements whenever they have a chance. They follow the boy scout rule when making those changes: Leave the code better than you found it.
Making sure engineers are heard
Management should always actively seek out and listen to the perspectives of the engineering team. In our view, engineers are best placed to identify potential technical debt before it occurs and suggest ways to avoid it. More often than not, their expertise can be leveraged to create more sustainable and performant solutions.
At Railsware, we promote an open feedback culture, where team members are encouraged to voice their opinions/concerns with their team (and the wider organization where appropriate). Dedicated Slack channels for squads and guilds are useful, but they shouldn’t replace synchronous communication. Management should make time in the day/week/month for well-planned meetings.
For example, daily standups aren’t the only meetings our Coupler.io engineers regularly attend. Many contribute to big-picture discussions, such as:
- Bi-weekly Iteration Planning Meetings (IPMs) to discuss upcoming work
- Bi-weekly product demos (involves all departments: engineering, marketing, design, etc.)
- Product retrospectives once a month
- Product strategy meetings with key stakeholders every 2 months
Not all engineers are required (or invited) to attend those meetings. But there are always at least one or two members of the team in attendance. This ensures that strategic decisions are made in collaboration with those who know the software best.
Embracing a flat(ish) organizational structure
Job titles and hierarchies often cause unnecessary blockers for agile development teams. However, we believe that the more autonomy developers have, the more empowered they are to build great products. In fact, adopting elements of a holacratic organizational structure has helped us develop a culture of ownership across all of our processes. In our version of holacracy, individuals within teams and guilds work closely to achieve a common goal, instead of relying exclusively on managers to assign roles and distribute tasks.
For example, our Mailtrap development team self-organizes into specialized guilds during each sprint. The main ones are:
- Frontend guild. Several developers work to push the project forward, keep frontend code clean, and introduce new approaches.
- Support guild. Here, developers devote their time to improving parts of the system where bugs have been found or tech debt alarms have been raised.
- OSS guild: Others work on open-source aspects of the product (public gems, SDKs).
- Security guild: This group of developers works on strengthening data security by improving data access processes, managing access to project assets, and so on.
In the context of tech debt prevention, this ‘guild’ approach has a couple of key benefits. Firstly, the team gets dedicated time and resources to make system improvements and address red flags in the codebase. Secondly, team members can work on different various parts of the project over several months. This lets them develop a deeper understanding of the project requirements and better contribute to product decision-making.
Investing time in pair programming
Pair programming refers to when two developers collaborate simultaneously on part of the project’s code. At Railsware, pair work helps us solve problems more efficiently and create smarter, more robust solutions. Sessions can run for as long as 2-3 hours, or as little as half an hour – it all depends on the complexity of the issue at hand. However, regardless of how long these sessions last, we believe the time investment is always worth it. When it comes to preventing tech debt, the primary benefits of pair programming are:
- Better quality code. With an extra pair of eyes on the code, it’s harder for mistakes or bad habits to slip through the cracks. Errors, bugs, and bad practices are fixed long before they have a chance to morph into tech debt.
- Ability to see the big picture. When engineers work in pairs, they are better equipped to evaluate all sides of a problem and keep the big picture in mind. (For example, the ‘Navigator’ investigates the context, which helps the ‘Driver’ shape the small details of implementation).
- Improved knowledge sharing. Team members have more opportunities to bond, share their skills, and develop a shared understanding of the product direction. This helps them to make better day-to-day decisions that support the product’s sustainable growth.
Conducting regular PR reviews
Regular and thorough pull request reviews are a key line of defense against the accumulation of tech debt. The practice enables teams to identify dysfunctional code and other code smells at an early stage.
So, to promote consistency and reliability, it’s important to establish a standard protocol for PR reviews. The more streamlined the process, the easier it will be to identify potential issues, communicate, and resolve them quickly. Here are some of our tips:
- Before sending a PR for review, authors should attempt to distance themselves from their code – try to look at it objectively, and within the context of the product. This way, they can correct some clear abnormalities and reduce some of the reviewer’s workload.
- For large and complex PRs, authors should create to-do lists in the PR description.
- Meanwhile, engineers should be proactive and review PRs at least once a day.
- Reviewers should ensure comments are clear, actionable, and respectful. See Conventional Comments for some great examples.
- When the PR is too big, or unclear, the reviewer should ping the author and do a synchronous pair review.
- Automate where possible. Our RoR developers often use Rubocop to maintain code formatting and catch potential issues. Simplecov is also useful for checking code coverage, as is Brakeman for identifying any security issues.
- The team should aim to close PRs within 2-3 days, otherwise, a build-up can occur and it’ll be harder to give each one the attention it deserves.
- When reviewers identify improvements that don’t directly relate to the purpose of the PR, they should approve it, but leave comments. Point out typos, better variable names or RSpec scenario titles, etc., but keep the project priorities in mind.
Nobody wants to deal with tech debt. But to prevent it from causing real harm to your product, it’s essential to have a clear understanding of what it is, and how it accumulates. It’s important to make sure that engineers are involved in decision-making processes. Not to mention, giving engineers more autonomy can empower them to take ownership of the technical debt and be more proactive in managing it. Conducting regular code reviews can help teams stay on top of their technical debt and prioritize the most critical issues. Meanwhile, pair programming can also help teams catch issues early on and ensure that code is up to the required standards.