Customise Consent Preferences

We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.

The cookies that are categorised as "Necessary" are stored on your browser as they are essential for enabling the basic functionalities of the site. ... 

Always Active

Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data.

Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.

Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc.

Performance cookies are used to understand and analyse the key performance indexes of the website which helps in delivering a better user experience for the visitors.

Advertisement cookies are used to provide visitors with customised advertisements based on the pages you visited previously and to analyse the effectiveness of the ad campaigns.

Identifying technical debt

Technical debt can have very real costs, from wasted developer time to security breaches. In this article we explore five key areas to look out for.

Technical debt: The trade-offs made—whether intentionally or unintentionally—that prioritise speed over long-term maintainability.

While these shortcuts may seem acceptable at the time of implementation, they often lead to increased system complexity, reduced maintainability, performance issues and higher costs over time.

Addressing technical debt is rarely straightforward, as dependencies on affected components may require significant modifications or even complete rewrites. In cases where resolving technical debt is not feasible, it becomes crucial to manage and evolve these systems effectively while minimising future risks.

By identifying and understanding technical debt, teams can improve backlog estimations, design new features with better isolation from problematic areas and gain greater control over system evolution.

Technical debt can be categorised into five major areas:

  1. Architectural Debt
  2. DevOps Debt
  3. Code Quality Debt
  4. Security Debt
  5. Testing & Documentation Debt.

In this article we will explore these categories, highlighting many overlooked issues.

 

1. Architectural Debt

Architectural debt affects the core structure of a system and is often the most challenging type of technical debt to address.

Poor architectural decisions can hinder scalability, prevent cloud adoption, introduce reliability and performance issues, increase operational costs and add unnecessary complexity, making long-term maintenance significantly more difficult.

Common examples of Architectural debt:

  • Lack of proper decoupling between system components, leading to tight coupling and reduced flexibility.
  • Using unscalable or platform-specific services (e.g. Task Scheduler tasks or Windows Services) for background jobs instead of cloud-native or cross-platform solutions.
  • Relying on server file systems for storage instead of scalable, distributed solutions. This creates a single point of failure, limits scalability and redundancy and blocks cloud migration since SaaS systems typically don’t rely on traditional file systems for file management.
  • Not using an ORM framework or failing to manage database changes through automated migrations.
  • Reinventing the wheel by implementing custom solutions for queuing, caching or distributing desktop/mobile app updates instead of using well-established technologies for these and similar tasks.
  • Bypassing APIs and using bad practices like direct database connections instead of a proper service layer.
  • Relying on outdated or unsupported technologies by using legacy technologies that hinder scalability, security updates and future development.
  • Failure to design for scalability and redundancy, which neglects the future growth of the system and its ability to handle increased load or geographic distribution, potentially causing serious limitations later on and hindering the system’s adaptability.
 

Real-world insight

As a cloud-native solution provider, we have strong foundations in place and don’t run into most of these problems. However, when taking over existing systems that weren’t designed for the cloud, we’ve seen our fair share of technical debt.

Some of these issues became real challenges, especially when trying to lift and shift a platform to the cloud, Azure in this case. While some problems can be worked around with clever solutions, like mounting an Azure File Share so an application that relies on a file system can continue treating it as one, others are much harder to fix.

Take, for example, a mobile application that directly interacts with the database without an API layer in between. Fixing architectural flaws in cases like this is difficult and they also come with major security risks. If database credentials are stored in the application, they can be extracted through decompilation (even with obfuscation, which only makes it slightly harder) or by accessing config files. Even if the database credentials are read from a secure or remote location, they can still be accessed by inspecting the memory. These kinds of issues make maintaining and securing the projects significantly more difficult.

 

 
2. Code Quality Debt

Code quality debt refers to suboptimal coding practices that compromise maintainability, reliability and overall system performance.

These issues may arise from rushed development, lack of code reviews or inconsistent coding standards. While the immediate impact may not be apparent, poor code quality accumulates over time, making it more difficult to add new features, fix bugs or ensure stability in the long run.

Common examples of code quality debt:

  • Lack of code reviews leading to undetected bugs and poor design decisions.
  • Not using object-oriented practices consistently, such as encapsulation, inheritance, polymorphism and neglecting SOLID principles, leading to fragmented code, reduced reusability and difficulties in maintaining or extending the system. This can result in increased complexity and tightly coupled code that is harder to test, refactor and scale.
  • Lack of modular design, which results in tightly coupled code and makes it difficult to isolate problems or introduce improvements, complicating system scaling, testing and maintenance.
  • Failure to follow best practices for resource management, concurrency and performance, such as not using transactions to ensure data consistency, incorrect use of synchronisation mechanisms (e.g., locks), improper resource disposal (e.g., not freeing connections or handles), inefficient management of HTTP connections (e.g., reusing connections or managing them through a connection pool), unnecessary use of delays or sleep calls and improper handling of asynchronous operations. These practices can lead to issues like data corruption, resource leaks, degraded performance and scalability problems.
  • Inadequate error handling or using empty catch blocks, making debugging and troubleshooting harder.
  • Ignoring performance optimisations or writing inefficient code that may impact the system’s performance, particularly in high-load or resource-intensive situations.
  • Large, complex methods or classes that are difficult to understand, maintain and test, violating the single responsibility principle.
  • Code duplication where similar logic is implemented in multiple places, making future changes error-prone and harder to maintain.
  • Lack of or insufficient comments and documentation in complex code sections, making it difficult for future developers to understand the logic and intent behind the code.
 

Real-world insight

As part of our experience across all our projects, we’ve found that well-executed code reviews are the most effective way to prevent bad code from being introduced. It’s crucial to establish a baseline quality standard for all projects being developed within the company.

In one of the projects we took over, we identified numerous code quality issues, primarily caused by a lack of experience among the developers and the absence of a proper code review mechanism. The project was not designed with decoupling in mind, the framework was not used correctly, there were no robust error handling mechanism (e.g., there were many empty catch blocks that essentially hid errors) and there were several other issues. It’s essential to analyse the codebase and identify these technical debts first. Not all of them may be worth fixing, so it’s important to prioritise carefully, considering which issues would provide the most value if addressed. For instance, improving error handling and introducing a more robust logging mechanism might not require significant effort and might very well be worth it to handle first, compared to redesigning and decoupling the modules, which would likely involve rewriting parts of the application.

The most effective way to avoid introducing this type of technical debt is by training the team to be more aware of best practices within frameworks and general coding principles while also establishing good software development practices, such as code reviews.

 
3. DevOps Debt

DevOps debt refers to inefficiencies, gaps, or shortcuts taken in the processes and tools used for automating the software delivery pipeline.

These issues often arise from prioritising speed over best practices, leading to difficult-to-maintain environments, increased risk of failure and slower development cycles. DevOps debt can result in increased operational overhead, inconsistent deployments and delayed releases.

Common examples of DevOps Debt:

  • Lack of a dedicated development or staging environment preventing accurate testing and validation of new features, leading to production issues and increased risk during deployment.
  • Lack of automated CI/CD pipelines leading to manual processes that slow down deployments and increase the risk of errors.
  • Over-reliance on ad-hoc or manual testing rather than automated tests integrated into the CI/CD pipeline.
  • Unmanaged or poorly managed configuration management (e.g., configuration settings scattered in the code rather than using centralised management systems like Azure App Configuration, Key Vault, etc.) leading to inconsistencies and deployment failures across different environments.
  • Lack of consistent logging, automated alerts and monitoring for key metrics and system health, leading to delayed detection of issues, prolonged downtime and difficulties in troubleshooting critical failures.
  • Inconsistent infrastructure as code practices causing difficulties in managing and provisioning infrastructure reliably.
  • Poor version management of deployed applications leading to inconsistencies in the application versions running across different environments, making it difficult to track changes, roll back to previous versions or ensure the correct version is deployed in production.
 

Real-world insight

Based on our experience with nearly all of the projects we work on, establishing solid DevOps practices can make a significant difference. However, implementing a fully automated system on an existing project often requires substantial effort.

CI/CD pipelines are at the heart of DevOps and automating build and release pipelines has become much simpler today, thanks to a wide range of powerful tools. However, creating these pipelines for an existing system can present challenges. For instance, if there are hard-coded configuration values in the code or configuration files, it’s essential to manage these properly in the CI/CD pipelines, as each environment will likely require different configuration sets (e.g., different database connection strings, feature flags, etc.). Additionally, automating some manual processes may require writing bash, batch or PowerShell scripts or commands, which can be time-consuming. For example, consider a desktop application that checks the latest released version by sending a GET request to an endpoint and expects a version number to be returned. If this process was previously done manually by someone updating the version after the release, it now needs to be automated, likely using CLI commands to automatically update the version in the relevant location.

 
4. Security Debt

Security debt refers to vulnerabilities or weaknesses within the system that arise from neglected security practices, shortcuts taken to expedite development or the failure to implement proper security measures from the outset.

This type of debt not only exposes the system to potential attacks but can also result in compliance issues, data breaches and significant long-term costs associated with remediation and reputational damage.

Common examples of security debt:

  • Custom security implementations (e.g., homegrown encryption algorithms or authentication mechanisms) rather than relying on widely adopted, vetted solutions (e.g., AES or RSA for encryption, OAuth or OpenID Connect for authentication).
  • Weak or outdated cryptography such as using MD5, DES, or other obsolete encryption algorithms that are vulnerable to modern attacks.
  • Improper handling of sensitive data (e.g., storing passwords in plain text, not using salt for password hashing or exposing sensitive data through unsecured channels).
  • Lack of proper input validation and sanitisation, leaving the system vulnerable to common attack vectors like SQL injection, cross-site scripting (XSS) and cross-site request forgery (CSRF).
  • Failure to follow security best practices outlined in OWASP Top Ten, including missing protections against common web vulnerabilities like insecure deserialisation and security misconfigurations.
  • Storing sensitive information in configuration files or hardcoding it within the code of distributed mobile or desktop apps, which can expose this data to attackers. Sensitive data such as API keys, passwords or encryption keys should be securely managed and encrypted and stored in protected environments (e.g., Azure Key Vault, AWS Secrets Manager, etc.) rather than within the application’s code or config files.
  • Inadequate access control mechanisms, such as missing role-based access control (RBAC) or improper enforcement of least privilege, leading to unauthorised access to sensitive information or functionality.
  • Unpatched third-party libraries or dependencies that introduce vulnerabilities, such as those found in outdated versions of frameworks, plugins or software packages.
 

Real-world insight

One of the worst security-related practices that can be implemented in a project is the creation of custom encryption or authentication mechanisms. There are many widely-adopted, thoroughly tested and secure solutions already in place and building your own introduces significant risks – essentially opening the door for potential exploits within a custom flow.

We encountered this type of technical debt in one of the projects we took over from a client. The project used a custom encryption mechanism alongside a weak encryption algorithm – MD5. These kinds of technical debts cannot always be fixed simply by updating the code, as additional steps may be required. For instance, resetting user passwords is necessary so they can enter their credentials again on their next login, ensuring that their password data is updated with strong, secure hashes. This step is crucial because passwords are not stored in plain text, so any update to the encryption requires a reset to apply the new, more secure method.

 
5. Testing & Documentation Debt

Testing and documentation debt arises when there is insufficient or outdated testing and documentation within a system, resulting in poor test coverage, unreliable tests and inadequate or missing documentation.

This type of debt can severely impact the long-term maintainability of a system, increases the cost of introducing new features and causes confusion for developers and stakeholders. Inadequate testing increases the likelihood of undetected bugs and issues, while poor documentation makes it harder for new developers to understand the system and for existing developers to efficiently maintain or extend the codebase.

Common examples of testing & documentation debt:

  • Inadequate test coverage leading to critical parts of the system not being tested, resulting in undetected bugs and higher risk of failure in production. Aiming for comprehensive unit, integration and end-to-end test coverage helps mitigate this.
  • Lack of automated tests integrated into the CI/CD pipeline, forcing manual testing that is error-prone, time-consuming and inconsistent.
  • Not having a clear test strategy or plan, leading to confusion about what to test and gaps in test coverage, which can leave critical functionality untested and increase the risk of issues in production.
  • Outdated or missing documentation, making it difficult for new developers to get up to speed with the system and hindering the ability to troubleshoot or extend functionality effectively. This includes missing API documentation, architecture diagrams or setup instructions.
  • Inconsistent or inadequate integration testing that doesn’t verify the interaction between different components of the system which can lead to integration issues in production environments.
  • Unreliable or flaky tests that frequently fail or provide misleading results, leading to wasted developer time in investigating issues that don’t exist or skipping tests due to low confidence in their reliability.
  • Lack of proper test environment setup or testing guidelines making it difficult for developers to replicate production-like conditions and identify issues early in the development process.
 

Real-world insight

One of the common challenges with testing and documentation is that they are often difficult to establish when starting a project from scratch, primarily due to budget constraints and the pressure to deliver quickly. As a result, they tend to be deprioritised until much later in the development process. We sometimes encounter this issue due to client budget limitations, but we prioritise implementing at least integration tests, as they provide the most value relative to cost.

Improving testing and documentation in an existing project can be time-consuming, especially for large codebases. For instance, introducing unit tests in a system with thousands of classes can be overwhelming. In such cases, prioritisation is key – starting with integration tests and then incrementally adding unit tests for critical or frequently modified components is often the most effective approach.

 

Summary

The cost of technical debt is undeniable. As an example, a 2022 study quantified a number of the costs associated with technical debt and poor code quality including:

  1. 15X more defects
  2. 42% of developers time being wasted
  3. Issue resolution taking 9X longer
 

Want help cutting down technical debt in your systems? Steer73 have a suite of services designed to help reduce technical debt while keeping cost, disruption and the time required from your team to a minimum.

Book a call →

Subscribe to our newsletter

For regular insights into UX, product management, innovation and technology, sign up to our newsletter.