Every Codebase Has Debt
Technical debt is not a sign of failure. It is a natural consequence of building software under real-world constraints. Deadlines, incomplete requirements, changing business priorities and evolving best practices all contribute to code that is good enough today but will slow you down tomorrow.
The problem is not that technical debt exists. The problem is when teams do not acknowledge it, do not track it and do not have a strategy for paying it down. Left unmanaged, technical debt compounds. What starts as a minor shortcut becomes a persistent source of bugs, slow deployments and frustrated developers.
What Technical Debt Actually Is
The term "technical debt" was coined by Ward Cunningham to describe the gap between the code you have and the code you wish you had. Like financial debt, it has a principal (the effort required to fix it) and interest (the ongoing cost of working around it).
Not all technical debt is created equal. Some is deliberate: a team makes a conscious decision to ship a simpler implementation now and improve it later. Some is accidental: the code was written without full knowledge of the requirements, and the resulting design does not fit the problem well. Some is environmental: the ecosystem has moved on, and what was best practice two years ago is now an anti-pattern.
Understanding which type of debt you are dealing with is essential for deciding how to address it.
How to Identify Technical Debt
The first challenge is finding it. Technical debt is rarely documented and often invisible to anyone who did not write the original code. Several signals can help you locate it.
Complexity hotspots
Files or functions with high cyclomatic complexity are strong candidates for technical debt. Complex code is harder to understand, harder to test and harder to change safely. Static analysis tools can measure complexity automatically and flag the worst offenders.
A function with a complexity score above 15 is almost certainly carrying debt. It likely grew incrementally, with each new feature or edge case adding another branch, another condition, another layer of nesting.
High churn files
Files that change frequently are worth investigating. High churn can indicate that a file is a "god module" that too many features depend on, or that the code is being repeatedly patched rather than properly fixed.
Combining churn data with complexity data is particularly revealing. A file that is both highly complex and frequently modified is the most dangerous kind of debt: it is hard to work with and you have to work with it often.
Coupled modules
When two files almost always change together, it suggests they are tightly coupled, even if they appear to be in separate modules. This kind of change coupling is a form of architectural debt. The module boundaries are not providing the isolation they should.
Dependency issues
Outdated dependencies, packages with known vulnerabilities and circular dependency chains are all forms of technical debt. They may not cause immediate problems, but they increase risk and make upgrades progressively harder.
Developer friction
Sometimes the best signal is qualitative. If developers consistently complain that a particular module is painful to work with, that is debt. If onboarding new team members takes weeks because the codebase is hard to navigate, that is debt. If deployments are slow or fragile, that is debt.
How to Prioritise
Not all debt needs to be paid down immediately. Some debt is cheap to carry and expensive to fix. Other debt is expensive to carry and cheap to fix. A good prioritisation framework considers both dimensions.
Risk vs effort matrix
For each piece of identified debt, assess two things: the ongoing risk of leaving it in place and the effort required to fix it. Plot these on a simple two-by-two grid.
High risk, low effort: Fix these immediately. They are actively causing problems and can be resolved quickly. Examples include a missing input validation on a public API endpoint or an outdated dependency with a known critical vulnerability.
High risk, high effort: Plan these into your roadmap. They are serious but require significant investment. Examples include replacing a tightly coupled data layer or migrating away from a deprecated framework.
Low risk, low effort: Fix these opportunistically. When a developer is working in the area, they can clean up the debt as part of the change. Examples include renaming confusing variables or splitting an overlong function.
Low risk, high effort: Leave these alone, at least for now. The cost of carrying the debt is low, and the cost of fixing it is high. Revisit periodically in case the risk profile changes.
Impact on velocity
The most important debt to address is the debt that slows down the work you are doing right now. If a team is spending 30% of its time working around a poorly designed module, fixing that module will have a greater impact on delivery speed than fixing a cleaner module that is rarely touched.
Track where developer time goes. If the same areas of the codebase keep generating bugs, requiring workarounds or blocking other changes, those are your highest-priority targets.
Strategies for Paying Down Debt
The 20% rule
Some teams allocate a fixed percentage of each sprint to debt reduction. Twenty percent is a common starting point. This approach ensures that debt is addressed continuously rather than accumulating until it forces a painful rewrite.
The key to making this work is discipline. The allocated time must be protected from feature creep. If debt reduction work is consistently pushed aside when deadlines approach, the strategy fails.
Boy Scout rule
"Leave the code better than you found it." When a developer modifies a file, they make one small improvement beyond the scope of their change: rename a confusing variable, extract a helper function, add a missing type annotation.
This approach is low overhead and distributes the work across the team. Over time, the cumulative effect is significant. The areas of the codebase that change most frequently (which are also the areas where debt matters most) improve steadily.
Targeted refactoring sprints
For larger pieces of debt, a dedicated refactoring effort is sometimes necessary. This works best when the scope is clearly defined, the expected outcome is measurable and the team has a way to verify that the refactoring did not introduce regressions.
Good candidates for targeted refactoring include replacing a deprecated library, breaking up a monolithic module, or standardising error handling across the codebase.
Automated quality gates
Quality gates on pull requests prevent new debt from entering the codebase. If a PR introduces a function with excessive complexity, adds a circular dependency or reduces test coverage below a threshold, the gate blocks it.
This does not reduce existing debt, but it stops the problem from getting worse. Holding the line is a prerequisite for making progress.
Incremental migration
When debt involves a large-scale change (migrating to a new framework, restructuring a database schema, replacing a core library), the strangler fig pattern is often the safest approach. Build the new implementation alongside the old one. Migrate consumers gradually. Remove the old implementation only when nothing depends on it.
This avoids the risk of a big-bang rewrite while making measurable progress.
Making Debt Visible
One of the most important things a team can do is make technical debt visible. If debt is invisible, it will not be prioritised. If it is not prioritised, it will not be addressed.
Several practices help:
Track debt explicitly. Create tickets for known debt, just as you would for bugs or features. Tag them so they can be filtered and reviewed. This prevents debt from being forgotten.
Measure quality trends. Track code quality metrics over time. Are complexity scores improving or degrading? Is the dependency tree growing or stabilising? Are new features being shipped with tests? Trends tell you whether your debt management strategy is working.
Report regularly. Include a quality summary in sprint reviews or engineering retrospectives. When the team and its stakeholders can see the state of the codebase, investment in debt reduction becomes easier to justify.
Frequently Asked Questions
How much technical debt is acceptable?
There is no universal answer. The right amount depends on the project's maturity, the team's velocity and the risk tolerance of the business. A startup building an MVP can tolerate more debt than a team maintaining a payment processing system. The key is to be deliberate about which debt you accept and to have a plan for addressing it.
Should we ever do a full rewrite?
Almost never. Full rewrites are one of the riskiest undertakings in software engineering. They take longer than expected, introduce new bugs and often end up replicating the same design problems. Incremental improvement is almost always safer and more effective.
How do we convince stakeholders to invest in debt reduction?
Frame it in terms of delivery speed. Technical debt does not slow down one feature; it slows down every feature. Calculate the time lost to workarounds, bug fixes and onboarding friction. Present debt reduction as an investment in future velocity, not as cleanup work.
Is there a way to measure technical debt objectively?
No single metric captures it completely. But a combination of signals, including complexity scores, churn rates, dependency health, test quality and documentation coverage, gives a reliable picture. The trend matters more than the absolute numbers.
Debt Is a Tool, Not a Failure
Technical debt is not something to eliminate entirely. It is something to manage deliberately. Teams that treat debt as a strategic tool, taking it on consciously when speed matters and paying it down systematically when the cost of carrying it grows, build software that remains healthy and productive over the long term.
The worst outcome is not having debt. It is having debt you do not know about, in code you rarely look at, compounding quietly until it forces a crisis. Visibility, prioritisation and consistent action are the antidote.