Technical debt quadrants, making debt visible, measuring interest, prioritising with business impact, debt registers, capacity allocation and communicating with product owners.
Technical debt is the future cost of a shortcut, and the interest is the extra effort or risk it adds every time we work near it. I make it visible in a debt register with each item's impact, how often we pay its interest (change frequency, incidents, delays) and a rough cost to fix. I prioritise by interest, not by how ugly the code is: debt in a hotspot that slows every feature beats a messy module nobody touches. I get time by explaining it in business terms (lead time, incident risk, cost), by reserving a steady share of capacity, and by pairing debt fixes with features that touch the same area.
"Technical debt is the future cost of a shortcut or an outdated decision. Ward Cunningham's metaphor works best when you keep both halves. The principal is what it costs to fix. The interest is the extra effort, delay or risk we pay every time we work near it. That distinction drives everything I do. I identify debt with evidence: Git hotspots (churn ร complexity), incident post-mortems, lead time per area, developer surveys, dependency and vulnerability scans, and .NET support lifecycle dates. I record it in a register with impact, interest, risk, rough fix cost and an owner. I prioritise by interest and risk, not ugliness: debt in a hotspot that slows every pricing feature beats a messy module nobody touches, and an out-of-support runtime with a date beats both. I get time by speaking business, not code quality: 'this adds three days to each pricing feature; we ship eight a quarter, so it costs 24 days a quarter, and fixing it takes 15'. Then I pay it down continuously (a steady capacity share, boy-scout fixes in code we touch, larger items bundled with related features) and prevent new debt with a definition of done, automated tests, architecture tests and automated dependency updates."
Fowler's technical debt quadrant:
| Prudent | Reckless | |
|---|---|---|
| Deliberate | "Ship now, refactor next sprint" (recorded, with a trigger) | "No time for design" |
| Inadvertent | "Now we know how it should have been" | "What's layering?" |
Other common forms: out-of-support runtimes (.NET Framework, end-of-life .NET versions), vulnerable/outdated packages, missing tests, missing observability, manual deployments, architecture that no longer fits the product or team structure, and documentation drift.
| Step | What I do |
|---|---|
| Identify | Hotspots, incidents, DORA metrics, surveys, dotnet list package --outdated --vulnerable --include-transitive, lifecycle dates |
| Record | Register entry: description, area, interest (days/quarter or incidents), risk, fix cost, owner, trigger |
| Prioritise | Interest ร frequency + risk (security, compliance, end-of-support dates) relative to fix cost |
| Communicate | Lead time, incident risk, cost of delay, deadlines; offer options |
| Pay down | Steady capacity share ([15โ20%]), boy-scout rule, bundle with related features, funded projects for big items |
| Prevent | Definition of done, code review, architecture tests, Renovate/Dependabot, CI gates on critical vulnerabilities |
| Interest | Cost to fix: low | Cost to fix: high |
|---|---|---|
| High | Do now | Plan and fund (with dates) |
| Low | Boy-scout when nearby | Record and leave |
Risk overrides: anything with a security/compliance deadline (end of support, critical CVE) jumps the queue regardless of interest.
| Avoid | Say instead |
|---|---|
| "The code is terrible" | "Pricing features take 3 extra days each; that's 24 days a quarter" |
| "We need a cleanup sprint" | "15 days of work pays back in under a quarter" |
| "It's best practice to upgrade" | "Support ends on [date]; after that, security patches stop and our [customer] audit fails" |
| One option | Options: fix now, fix with feature X, or accept the risk explicitly (and record who accepted it) |
Our [order service] ran on an out-of-support .NET version with [14] vulnerable packages. Earlier upgrade requests had been framed as "code quality" and kept losing to features. When a customer security questionnaire flagged unsupported software, I reframed it as a security and compliance risk with a date and a cost: the vulnerabilities, the end-of-support date, the audit finding, and a [six-week] plan. It was approved. We upgraded project by project to .NET 10 LTS (using the .NET Upgrade Assistant and multi-targeting where needed), fixed the vulnerable packages, and added Renovate plus a CI gate that fails on critical vulnerabilities. Lifecycle dates now live in the debt register, so the next upgrade is planned, not urgent.
Checks leadership, prioritisation and stakeholder communication.
Green flags
Red flags
Ward Cunningham (1992) introduced "technical debt" to explain to business stakeholders why shipping early with imperfect understanding is fine if you repay it by refactoring as you learn. The metaphor was about learning, not about permission to write sloppy code. Martin Fowler's quadrant later separated prudent/reckless and deliberate/inadvertent debt.
| Meaning | Example | |
|---|---|---|
| Principal | One-off cost to fix | 15 days to split the pricing god class |
| Interest | Recurring cost of not fixing | 3 extra days per pricing feature; 2 incidents a quarter |
| Payback period | Principal รท interest per period | 15 รท 24 per quarter โ 0.6 quarters |
High-principal, low-interest debt (messy code nobody touches) can stay. Low-principal, high-interest debt should go first.
| Source | Shows |
|---|---|
| Git churn ร complexity (hotspots) | Where interest is paid most often |
| DORA metrics (lead time, change failure rate, MTTR) | Delivery drag and instability |
| Incident tags by component | Reliability interest |
| Build/test duration trends | Feedback-loop interest |
dotnet list package --vulnerable --include-transitive |
Security risk |
| .NET support policy | Deadlines: LTS releases supported 3 years; STS releases 24 months (since .NET 9) |
| Developer surveys | Where people avoid working |
| Mechanism | Best for |
|---|---|
| Boy-scout rule | Small improvements in code you're already changing |
| Steady capacity share | Medium items; predictable progress |
| Bundle with features | Debt in the area a feature touches ("refactor pricing as part of loyalty discounts") |
| Funded project with dates | Runtime upgrades, architecture migrations |
| Stop-the-line | Critical vulnerabilities, data-loss risks |
dotnet list package --vulnerable --include-transitive and --outdated on your solution.// A lightweight debt register entry model used to score and sort the backlog.
var register = new List<DebtItem>
{
new("Pricing god class", Area: "Pricing", InterestDaysPerQuarter: 24, FixDays: 15, Risk: Risk.Medium),
new("Out-of-support runtime", Area: "Orders", InterestDaysPerQuarter: 2, FixDays: 30, Risk: Risk.Critical),
new("Messy CSV importer", Area: "Admin", InterestDaysPerQuarter: 0.5, FixDays: 10, Risk: Risk.Low)
};
foreach (var item in register.OrderByDescending(d => d.Score))
Console.WriteLine($"{item.Score,6:F1} {item.Title} ({item.Area}) payback {item.PaybackQuarters:F1} quarters");
public enum Risk { Low = 1, Medium = 2, High = 4, Critical = 8 }
public sealed record DebtItem(string Title, string Area, double InterestDaysPerQuarter, double FixDays, Risk Risk)
{
// Interest paid per year, weighted by risk, relative to cost to fix.
public double Score => (InterestDaysPerQuarter * 4 + (int)Risk * 5) / FixDays * 10;
public double PaybackQuarters => InterestDaysPerQuarter > 0 ? FixDays / InterestDaysPerQuarter : double.PositiveInfinity;
}
The exact formula matters less than making interest and risk explicit and comparable.
What happens: all messy code labelled debt; a vague request for time. Why it is wrong: sounds like gold-plating; rarely approved or finished. Do instead: specific items with interest, risk and payback, funded steadily.
What happens: effort goes to the code that offends engineers most. Why it is wrong: often code that rarely changes, so little return. Do instead: prioritise hotspots and dated risks.
What happens: "coupling", "god class", "SOLID violations". Why it is wrong: they can't weigh it against features. Do instead: lead time, incidents, cost, deadlines, options.
What happens: runtimes and packages drift out of support. Why it is wrong: quiet debt becomes urgent security/compliance risk. Do instead: track lifecycle dates; automate dependency updates.
What happens: "we'll fix it later" with no ticket. Why it is wrong: nobody remembers; interest compounds. Do instead: log each shortcut with an owner and trigger.
What happens: "it's beyond saving". Why it is wrong: high risk, long freeze, lost behaviour. Do instead: incremental refactoring with strangler fig / branch by abstraction.
What happens: frustration without a plan or a decision-maker involved. Why it is wrong: no funding, no accountability. Do instead: review the register with product owners regularly.
What happens: refusing any shortcut under deadline pressure. Why it is wrong: prudent, deliberate debt can be the right business call. Do instead: take it consciously, record it, plan repayment.
What happens: improvements made but not shown. Why it is wrong: the next request lacks credibility. Do instead: report outcomes (lead time, incidents, build time).
What happens: "we'll live with it" with no named decision-maker. Why it is wrong: when it bites, nobody knew it was a choice. Do instead: record who accepted the risk and when to revisit.
Seniors manage debt like a financial portfolio: some debt is a sensible loan, some is ruinous. The skill is quantifying interest, communicating it in business terms, and paying it down without stopping delivery.
| Debt type | Typical strategy |
|---|---|
| Hotspot code design | Refactor with related features |
| End-of-support runtime | Dated plan, treated as security work |
| Vulnerable packages | Automate updates; fix critical immediately |
| Missing tests | Add with each change to the area |
| Architecture misfit | ADR, phased migration |
| Low-churn messy code | Record and leave |
I treat a large refactoring as a series of small, behaviour-preserving steps merged to main continuously, not a big-bang branch. First I pin current behaviour with characterisation tests. Then I pick a technique: strangler fig to route traffic piece by piece to a new implementation, branch by abstraction to put an interface in front of the old code and swap implementations behind it, or parallel change (expand, migrate, contract) for APIs and database schemas. Feature flags and shadow comparisons let old and new paths run side by side. I prioritise hotspots (code that changes often and hurts), keep refactoring commits separate from feature commits, and define an end state so it does not drag on forever.
Over-engineering is complexity that today's requirements do not justify. I detect it through symptoms with numbers behind them: a one-field change touching many files, interfaces with one implementation, configuration options nobody sets, patterns like CQRS or event sourcing without the problems they solve, and microservices that always deploy together. I reverse it incrementally rather than with a rewrite: inline single-use abstractions, merge layers that only pass data through, consolidate services that share a release cycle, and delete unused options, each step behind tests. I also address why it happened, because otherwise it grows back.
Evolutionary architecture (Ford, Parsons and Kua) is architecture designed to support guided, incremental change, rather than a fixed upfront design. The key tool is fitness functions: automated checks that protect the characteristics you care about, such as dependency rules (architecture tests), latency budgets, or startup time, run in CI or production. Around that, I keep boundaries modular (often a modular monolith first), make decisions reversible where possible and defer irreversible ones to the last responsible moment, record decisions in ADRs, and rely on continuous delivery, feature flags and observability so each change is small and safe.
How to tell it (STAR)
Our order service was on an unsupported .NET version with [14] vulnerable packages, and earlier requests to upgrade had been framed as code quality. I reframed it as a security and compliance risk with a deadline and a cost, and got [six] weeks approved. We upgraded project by project to .NET 10 and automated package updates so it does not happen again.
Last updated 31 Oct 2026.