We have all been working on an existing project with many issues. All of those issues are more or less known to team members and are, to some extent, apparent problems in the development flow. The umbrella term for all of these issues is Technical Debt.
Disclaimer: The article is written in cooperation with CodeScene.
Catio: Your Copilot for Tech Architecture (Sponsored)
Transform your tech stack with Catio: your one-stop solution for tech architecture management and planning. Trusted by CTOs, architects, and tech leads, Catio delivers advanced observability, 24/7 AI-driven recommendations for improvements, and data-driven planning capabilities for your tech architecture. Ready to architect with AI expertise and excel with your tech stack? Book a demo today!
What is technical debt?
There are many ways to define Technical debt. But in simple English terms, imagine you have a leaky roof. Instead of fixing it properly, you use a bucket to catch the water. This quick fix solves the immediate problem, but you must empty the bucket every time it rains. Over time, the bucket starts to overflow, causing more damage and requiring even more work to manage the leak.
This is similar to technical debt in software development. When you're under pressure to deliver a project quickly, you might take shortcuts or implement quick fixes instead of designing a robust solution. These shortcuts help you meet your deadline but create issues that must be addressed later. Like with the leaky roof, these issues can accumulate, making your system harder to maintain and more prone to problems.
For example, if you write messy code to get a feature out the door faster, that messy code is technical debt. It might work for now, but it will likely cause bugs and be challenging to understand and improve in the future. The more technical debt you have, the more resources you'll need to allocate to fixing and managing these problems instead of working on new features or improvements. Some other shortcuts are outdated libraries, ignoring proper testing and documenting, etc.
According to research conducted by Stripe Inc. in September 2018 and published in the research paper The Developer Coefficient, "developers spend circa 13,5 hours on technical debt, and including bad code, it's 17,3 hours," given an average workweek of 41,1 hours. So, considering meetings and other non-productive events, we have a maximum of 2 to 3 days of work per week.
In general, Technical Debt is a business problem. We can use technical debt as a financial loan. It's like borrowing money when you take a shortcut in your software development to meet a deadline or cut costs. This borrowed "time" comes with interest. Over time, the interest accumulates, requiring more effort to maintain and fix the system.
But why should businesses care about technical debt? Because it’s not just a technical issue—it’s a significant business problem. Here’s why:
Impact on speed: Technical debt slows down your development process. When developers spend more time fixing bugs and maintaining poorly written code, they have less time to innovate and create new features. This delay can put you behind competitors who can move faster and adapt more quickly to market changes.
Increased costs: Like high-interest loans, technical debt increases costs over time. The longer you delay addressing it, the more expensive and time-consuming it becomes. This can divert resources from other critical business initiatives, impacting your overall budget and profitability.
Dissatisfied users: Technical debt often leads to more bugs and system issues, which can degrade the quality of your product. Poor product quality can result in unhappy customers, negative reviews, and increased support costs. Maintaining high-quality standards is crucial for customer retention and brand reputation.
Risk management: Accumulated technical debt increases the risk of system failures and security vulnerabilities. These risks can lead to costly downtimes, data breaches, and regulatory penalties. Proactively managing technical debt helps mitigate these risks and ensures smoother, more secure operations.
Longer development cycles: As tech debt increases, developers' inability to operate within the current code base causes them to divide their time between creating new features and fixing bugs, slowing down the software development lifecycle and extending the time to market.
Yet, when we refer to Technical Debt, we usually mean “bad code,” even though bad code presents just one aspect of it.
What is legacy code?
Legacy code refers to software systems or code bases that are outdated, difficult to maintain, and often inherited from previous development efforts. It is code written long ago, typically using older technologies, programming languages, or development practices that are no longer considered best practices.
Some say that all code is legacy immediately when we write it, which is true to some extent. Most importantly, it represents the business risk because it is hard to maintain or change. Why is it hard to change?
Here are a few key reasons:
We have old technologies that are no longer maintained (compilers, platforms), but they are not common technologies on the market, so developers don’t know about them.
There is no documentation, so it’s hard to understand what is happening.
People who worked with the codebase are no longer in the company.
It often lacks comprehensive automated tests, making introducing changes without new bugs challenging.
Legacy codebases are often tightly coupled and lack modularity, making it difficult to introduce changes without introducing new bugs or breaking existing functionality.
Every new feature we have in our apps adds maintenance load.
When we look at these issues, many lead us to the concept of enlarged Technical Debt, but they are different. Legacy code is outdated and hard to change, and Technical Debt is extra work we need in the future because we took some shortcuts.
The problems with Technical debt and legacy code
When we think about why legacy code, together with Technical Debt, is a big problem, here are the main reasons:
Competitiveness: Companies heavily reliant on legacy systems and burdened by technical debt may struggle to keep up with more agile, innovative competitors, leading to market share and revenue losses.
Security and compliance risks: Outdated legacy systems often contain vulnerabilities and may not meet modern security and regulatory requirements, exposing the company to legal, financial, and reputational risks.
Reduced organizational agility: Legacy systems' rigidity and inflexibility can hinder a company's ability to adapt to changing market conditions, customer needs, or technological advancements, limiting its long-term growth and resilience.
Increased project timelines and costs: Addressing technical debt and legacy code often requires significant time and resources, leading to project delays, budget overruns, and reduced efficiency.
Stagnant innovation. The most critical cost of ignoring technical debt is its impact on innovation. When technical debt is substantial, developers must continue to service existing issues rather than invest time to create innovative, business-value capabilities. This situation severely limits an organization's ability to respond rapidly to market opportunities or changes.
Difficulty attracting and retaining talent: Developers and engineers are often reluctant to work with outdated technologies and complex legacy codebases, making it challenging for companies to attract and retain top talent.
The last item becomes even more important when we consider the latest research, such as the 2024 Stack Overflow Developer Survey, which marks Technical Debt as the No. 1 frustration among developers.
Generally, Technical Debt in legacy systems usually represents businesses that earn money, reduce productivity, and increase maintenance costs.
Yet, not all legacy code is bad. One key issue with legacy code is that it is only sometimes easily visible. Legacy systems may be deeply embedded within an organization, powering critical business functions without drawing much attention. These systems often work as intended, delivering value day in and day out, but their inner workings are only sometimes apparent to those outside the immediate development team.
This lack of visibility can lead to misconceptions about the quality and importance of legacy code. It's easy to overlook that these systems have been silently powering the organization for years, if not decades. The code may not be the latest and greatest, but it serves its purpose effectively.
What is bad code?
Rather than automatically dismissing legacy code as "bad," taking a better approach is important.
Kent Beck came up with his four rules of good code design while he was developing Extreme Programming in the late 1990s:
Passes the tests. The code should pass all the automated tests. This ensures the software behaves as expected and helps prevent regressions. Writing tests first (test-driven development) can guide the design and provide robust functionality.
Reveals intention. The code should be clear and readable, making what the programmer intended to achieve obvious. This involves using meaningful names for variables and functions and structuring code in a way that communicates its purpose.
No duplication. Eliminating redundancy in code makes it easier to maintain and reduces the risk of errors. This includes avoiding duplicate logic, data, and structures and simplifying the codebase.
Fewest elements. Keeping the codebase as simple as possible without unnecessary complexity helps maintain and understand the code. This includes using the fewest classes and methods to achieve the desired functionality.
We can also say that good code is like a joke; you don't need to explain it.
In addition to this, bad code has the following characteristics:
Bad readability. Imagine trying to read a book where every sentence is a run-on or lacks punctuation. Bad code is similar—it's hard to read and understand. Poor naming conventions, lack of comments, and convoluted logic make it difficult for others (and sometimes even the original author) to follow the code.
Poor structure. Bad code often lacks a coherent structure. This includes poor organization of files and functions, excessive nesting, and a lack of modularity. A good codebase should have a clear, logical structure where each part has a distinct responsibility.
Bad reusability. Repetition is a hallmark of bad code. If you find the same block of code copied and pasted in multiple places, that's a red flag. Good code uses functions and modules to promote reusability, making it easier to maintain and update.
Ignoring best practices. Good coding practices evolve from years of collective experience in the industry. Ignoring these, such as proper error handling, adherence to design patterns, or following coding standards, can lead to bad code.
Performance issues. Bad code can include inefficient or poor-performing computations, memory leaks, or inefficient algorithms that could have been optimized.
Wasteful: Bad code has a measurable, negative impact on developer productivity.
But how can we measure this? We start by looking for smells in the code and then refactoring it.
Mitigation strategies
To reduce technical debt in legacy projects, we need to take a two-step approach:
Measure Technical Debt. First, we need to analyze our codebase and try to understand its maintenance load. Maintenance load measures the ongoing effort required to keep existing features operational rather than just evaluating the code's "shittiness." This approach helps teams understand the cost of technical debt in terms of developer time and resources, providing a clearer picture of its impact on productivity and progress. Here, we can also use metrics, such as DORA, SPACE, or DevX, with indicators for availability, deployment frequency, mean time to restore, and more. We can use tools such as CodeScene to measure and deal with Technical Debt in code graphically, in terms of readability, quality, issues, and other areas, and express this in a way that can be seen easily from the business side.
Reduce Technical Debt. We must identify the areas with the highest maintenance load to reduce technical debt. Then, we prioritize changes based on their return on investment, focusing on high-leverage code modifications that can significantly reduce ongoing maintenance costs.
Along with this intentional reduction of technical debt, we should try to keep the Technical debt low on an ongoing basis, and here are a few strategies to do it:
Write documentation for all your architectural decisions (ADRs) and technical documentation. Make it easy to find (place it in the repo near the code).
Write tests on all levels of the Testing Pyramid, mostly unit tests. Aim at 60-80% of code coverage.
Refactor constantly. When you touch something, always try to improve it. Don’t ask for permission, especially if it can be done in a few hours. If you cannot refactor, add a comment for complex code that explains it briefly.
Encourage a code review culture. Involve everyone in code reviews and check for code readability, design, performance, security, testability, and documentation. It is one of the best ways to share knowledge.
Follow codebase standards, such as design, naming, and architecture. Then, automatize this through tools such as linters or your CI/CD process.
Read other strategies for reducing Technical debt here:
Yet, in general, we should write less code to solve business problems, as all code becomes Technical Debt that should be maintained.
Using Behavioral Code Analysis to Fix Technical Debt
In this article, we will see how to use a unique approach called “Behavioral Code Analysis” (supported by the CodeScene tool) to manage and reduce Technical Debt. In his book “Software Design X-Rays,” Adam Tornhill offers an original method to address this issue. Combining software architecture with human psychology produces strong methods for handling huge codebases. This approach focuses on answering the following three questions:
Where’s the code with the higher interest rate?
Does your system's architecture enable its evolution?
This approach consists of the following steps:
1. Identity code with high interest rates
This approach emphasizes the code that changes more often. Code that has not been touched for a long time and that we don’t need to maintain is usually not a problem, and that is usually the majority of code in the codebase. Similar static analysis tools alarm code smells all over the code, but the key is that our focus should be on the code that changes a lot.
2. Prioritize Technical Debt with Hotspots
When we know which code has changed a lot, we need to find hotspots. Hotspots are the complicated code that you work with often. Hotspots are calculated in the following way:
Calculating the change frequency of each file as a proxy for interest rate
Using the lines of code as a simple measure of code complexity or more elaborate metrics like Code Health.
Here is an example of hotspots in a codebase:
We can easily decide where to refactor based on hotspots by focusing on files with significant complexity and churn. Complexity can be Cyclic complexity, and churn is the number of times it has changed. To understand this better, we can use the following graph.
What we don’t want to do with this is to try to “fix everything.” We want to free ourselves outside of Omega Messes. Omega Mess is mostly issued on the edge of our software systems, in files opened occasionally. We want to ignore these messes and focus on code with high churn.
3. Evaluate Hotspots with Complexity Trends
When we find our hotspots, we need to check how severe a potential problem is via complexity trend analysis with complexity trend over time. This trend tells us what happened with our hotspots in the past. The example below shows that it grew and exploded just before 2021. It means it's harder and harder to fix.
4. Use X-rays to get insights into the code.
Then, when we narrow it down to a single file where improvements are needed, we usually have enough information to improve the code. CodeScene enables us to see a prioritized list of methods, inspect them, and refactor them.
An example of analysis for NopCommerce Project
Now, we will see how to use CodeScene, using the example of the popular open-source project NopCommerce, which is built as an ASP.NET application. NopCommerce is a popular open-source eCommerce platform built on the .NET framework. It is designed to be a solution for businesses of all sizes, providing a flexible, scalable, and highly customizable system for online stores created in 2008.
The architecture of NopCommerce is modular and layered, adhering to standard software development principles that ensure maintainability and scalability. It uses the Onion architectural style and is built on the ASP.NET Core framework, which allows it to run on various operating systems, including Windows, Linux, and macOS. The platform follows a traditional three-tier architecture comprising the presentation, business logic, and data access layers.
Presentation Layer (Nop.Web): The front end is powered by ASP.NET Core MVC, providing a flexible and responsive user interface. Razor views allow for easy customization of the storefront's look and feel. This layer also supports themes and plugins, enabling store owners to extend and modify the front end without altering the core codebase.
Business Logic Layer (Nop.Services): The heart of the application, this layer handles all the business rules and operations. It includes services, managers, and other components that process input from the presentation layer, interact with the data access layer, and enforce business logic. This separation of concerns helps maintain clean, testable, and reusable code.
Data Access Layer (Nop.Data): NopCommerce uses Entity Framework Core for data access, which provides an abstraction over the underlying SQL Server database. This layer controls CRUD operations, database migrations, and data integrity. The use of Entity Framework Core also facilitates the potential for multi-database support in the future.
Now that we have a better understanding of the project and its architecture let’s start the analysis using the CodeScene tool. When you run the analysis for the first time, you will get the following dashboard: You can check the code health trends for one week, month, and year.
From here, we can see that the Hotspot Code Health score is 3.8, marked in red. This means there is a lot of code with severe technical debt inside. The image below defines the scale: we should aim at a score of 9 or higher.
When we want to go deeper, we can go to the Hotspots view, which will show how much development is done on different project parts, or to the Code Health tab to see problematic code (using static analysis).
If we want a unified diagram showing problematic code that changes frequently, we can go to the Refactoring Targets view, as shown in the image below.
On this level, we can click on the problematic file, marked in red, an InstallationService.cs file, as shown in the image below.
On the right part of the image, we have two options:
Review: When we select this, it will go to the screen where we can see rule violations and warnings in the file. Here, we can also plan a goal for this hotspot. In addition, at this level, we can see a complexity trend on the file over time.
X-Ray: This brings a prioritization view, where we can see an overview of every method, with change frequency, issues found, and function complexity, as shown in the image below (all Code Health rules are documented here). Change Coupling Details give more insights about the coupling in the codebase.
In the example, we see two particular problematic methods:
InstallSettingsAsync(): This method has a high change frequency and a lot of lines of code (857)! CodeScene marked it as a “Complex Method,” meaning it probably has many conditional if statements, which are hard to track and understand (Cyclomatic Complexity).
InstallCustomersAndUsersAsync(): This method is marked as large. It has 138 lines of code and a high change frequency. In general, we need methods with less than 70 lines of code.
From here, we can start planning and implementing the fix for the particular issue. When we finish it and push changes to the Git repo, we can run the analysis again to see if our codebase health has improved.
Another convenient option for working with CodeScene is the VS Code tool (which is free), which shows code issues in the text editor and enables you to react to any issue that can happen immediately. In the example of the ValidateCustomerAsync() method in the image below, we can see the one marked as a Complex Method.
When we refactor it, we get the fixed code without warning, which can also be visible on the CodeScene dashboard after the next analysis.
Conclusion
Managing technical debt is crucial for maintaining a competitive edge in software development. High code quality is not just a technical requirement; it's a strategic business advantage.
Healthy, maintainable code can lead to a remarkable 124% increase in development speed compared to unhealthy code. This improvement directly impacts developer productivity, enabling teams to deliver features more quickly and reliably. When code is easier to understand, modify, and extend, developers spend less time wrestling with complex issues and more time creating value. As a result, the overall efficiency of the development process is significantly enhanced, contributing to faster release cycles and better software quality.
Keeping technical debt under control is crucial for optimizing resource allocation within an organization. Codebases that maintain high quality have been shown to experience 15 times fewer defects. This reduction in bugs translates to less time spent on unplanned work, allowing teams to focus on strategic initiatives that drive the business forward. By minimizing the distractions caused by technical debt, organizations can achieve more predictable delivery timelines and better resource utilization, ultimately leading to a more streamlined and effective development process.
Poor code quality introduces significant risks, particularly in terms of unpredictability. Bad code can take exponentially longer to complete, creating uncertainty that can severely impact productivity and project timelines. By prioritizing improvements in code quality, companies can mitigate these risks, leading to more reliable project outcomes. This reduction in uncertainty not only decreases operational stress but also enhances the overall stability of the software development lifecycle, ensuring that projects are delivered on time and within budget.
Healthy code also plays a critical role in reducing uncertainty in task completion times by a factor of nine. This level of predictability is essential for accurate feature planning and effective roadmap execution. With clearer timelines, product managers can make informed, data-driven decisions that balance the need for new feature development with the necessary refactoring efforts to maintain code health. This balanced approach ensures that the development process remains aligned with the company’s strategic goals, supporting long-term growth and success.
Prioritizing technical debt management is vital for aligning technical efforts with broader business objectives. Organizations can improve productivity, free up resources, and reduce risks by focusing on managing technical debt. These improvements lead to better strategic decision-making and more effective execution of the company’s roadmap. Additionally, treating code quality as a key business performance indicator (KPI) ensures that investments in technology directly contribute to the company’s competitive advantage and long-term success. This alignment between technical and business goals is critical for sustaining growth and maintaining a good position in the market.
More ways I can help you
LinkedIn Content Creator Masterclass ✨. In this masterclass, I share my proven strategies for growing your influence on LinkedIn in the Tech space. You'll learn how to define your target audience, master the LinkedIn algorithm, create impactful content using my writing system, and create a content strategy that drives impressive results.
Resume Reality Check" 🚀. I can now offer you a new service where I’ll review your CV and LinkedIn profile, providing instant, honest feedback from a CTO’s perspective. You’ll discover what stands out, what needs improvement, and how recruiters and engineering managers view your resume at first glance.
Promote yourself to 34,000+ subscribers by sponsoring this newsletter. This newsletter puts you in front of an audience with many engineering leaders and senior engineers who influence tech decisions and purchases.
Join My Patreon Community: This is your way of supporting me, saying “thanks, " and getting more benefits. You will get exclusive benefits, including all of my books and templates (worth $100), early access to my content, insider news, helpful resources and tools, priority support, and the possibility to influence my work.
1:1 Coaching: Book a working session with me. 1:1 coaching is available for personal and organizational/team growth topics. I help you become a high-performing leader and engineer 🚀.
Gained a lot of knowledge from this piece. Thank you so much for this eye opening article.
Our charity is absolutely riddled with tech debt as they have historically been using band aids, messy code to keep things propped up. I’m ripping the band aid of as it is just costing us too much. Parts of your article will be great to really drive the point home. Obviously I will credit yourself sir 👍🏾👍🏾