Tech debt at scale: hotspots and a debt portfolio
Debt interest gathers where complex code changes often: rank files by commits times complexity, fund the top, and judge it by that area's delivery.
Predict first
Before you read: predict
ExamHall's tech lead wants next quarter's debt work to go into rewriting exports/legacy-pdf.ts, the longest file in the monorepo. Before the planning meeting the team runs a hotspot analysis over the last six months: code-complexity 4.4.4, with lines of code as its complexity measure. Today is 15 April 2026.
file lines of code commits since 15 Oct of them fix: commits people committing
grading/rules.ts 1,900 41 6 5
proctor/flags.ts 60 88 10 9
i18n/messages.ts 900 22 1 14
candidate/autosave.ts 420 35 30 4
exports/legacy-pdf.ts 2,400 0 0 0
Which file does the analysis put first?
The 30-second version
Interest is paid on changes, and version control records where changes go. Count each file's commits over recent months, multiply by a complexity measure such as lines of code, and the top of that list is where debt costs most. Pay it from a standing share of capacity or an outcome-framed project, and watch how changes there move afterwards.
Debt interest gathers where complex code changes often: rank files by commits times complexity, fund the top, and judge it by that area's delivery.
How it works
Step 1 of 14
se-technical-debt step 2); version control records every change.In the kitchen
- the till's record of every hire → version control's record of every change (step 1)
Analogy: A tool-hire depot's workshop
| At the depot | In the codebase |
|---|---|
| the till's record of every hire | version control's record of every change (step 1) |
| hires per tool this season | commits per file in the window (steps 2, 6) |
| the workshop's wear grade on each tool | lines of code as the complexity measure (step 3) |
| hires times wear, worked down from the top | the hotspot list (step 4) |
| one workshop day a week, every week | a standing share of capacity (step 9) |
| hires cut short by a breakdown, before and after the service | the area's changes, before and after (steps 11, 13) |
In real terms: count where changes go, weight each file by how hard its code is, and spend a steady share of time at the top of that list, judged by how changes there go afterwards.
Breaks down when: a worn tool fails outright, while tangled code keeps working and only makes each change slower and riskier.
Contrasting case: a display of antique tools that are never hired out: wear with no hires costs the depot nothing.
Examples, run for real
4.1 ExamHall EXH-4370: the rewrite proposal, checked against six months of history
EXH-4370: ExamHall's tech lead proposed spending next quarter rewriting exports/legacy-pdf.ts, the longest file in the monorepo. Before deciding, the team ran code-complexity 4.4.4 over the last six months of the fixture monorepo. MR !4670 records the list and funds work on its top file instead.
4.1 EXH-4370 code-complexity 4.4.4, lines of code, 2025-10-15 to 2026-04-15: 7 of 8 files listed
4.1 #1 grading/rules.ts 41 commits × 1,900 lines = 77,900
4.1 #2 i18n/messages.ts 22 commits × 900 lines = 19,800
4.1 #3 candidate/autosave.ts 35 commits × 420 lines = 14,700
4.1 #4 proctor/flags.ts 88 commits × 60 lines = 5,280
4.1 #5 reports/term-summary.ts 6 commits × 700 lines = 4,200
4.1 #6 grading/export-csv.ts 9 commits × 300 lines = 2,700
4.1 #7 candidate/timer.ts 12 commits × 150 lines = 1,800
4.1 exports/legacy-pdf.ts: 2,400 lines, last commit 2022-06-11, not listed
4.1 MR !4670 funds grading/rules.ts, the top entry
The exporter is 2,400 lines, but nobody committed to it in six months, so the tool does not list it (steps 2, 5). grading/rules.ts is shorter and changed in 41 commits, so it tops the list by a wide margin (step 4). proctor/flags.ts has the most commits, but at 60 lines each change is cheap.
4.2 MR !4670: the grading area, before and after
MR !4670 replaced the if-chains in grading/rules.ts with one rule table per question type, in the same file, paid for from a standing share of the grading team's sprints. It landed in May 2026. The team compared the changes to grading/rules.ts in the first quarter of 2026 with those in the third. The records are synthetic.
4.2 changes to grading/rules.ts, Q1 2026, before MR !4670: 18 · median lead time 6 days · needed a rollback or hotfix: 5 of 18 (28%)
4.2 changes to grading/rules.ts, Q3 2026, after it: 21 · median lead time 2 days · needed a rollback or hotfix: 2 of 21 (10%)
4.2 MR !4670's own commits: 14, 2,300 lines added + deleted
The refactor's own size says the work was done; the area's changes before and after say what it bought (steps 11–13).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A ticketing platform's team will fund one refactor next quarter and builds a hotspot list from its repository's history to choose it, with lines of code as the complexity measure. Today is 15 October 2026. src/generated/ticketing-api.ts is rewritten by openapi-generator from the API spec whenever the spec changes; nobody edits it by hand.
file lines of code commits, last 12 months commits before that
src/generated/ticketing-api.ts 3,000 48 10
pricing/legacy-fees.ts 2,200 2 95
venue/seat-map.ts 1,400 18 6
reports/sales-export.ts 900 10 12
seat-hold/holds.ts 800 40 20
checkout/cart.ts 300 64 8
Which file should top the team's hotspot list?
Question 2 of 8
In June the same team split seat-hold/holds.ts, the top of that list, into a slimmer seat-hold/holds.ts plus seat-hold/expiry.ts and seat-hold/release-rules.ts. These are its changes to seat-hold/ in the quarter before the split and the quarter after. Each change is one commit, and its lead time runs from that commit to production:
change quarter files touched lead time (days)
SH-101 Q2 seat-hold/holds.ts 9
SH-102 Q2 seat-hold/holds.ts 12
SH-103 Q2 seat-hold/holds.ts 7
SH-104 Q2 seat-hold/holds.ts 10
SH-105 Q2 seat-hold/holds.ts 8
SH-131 Q3 seat-hold/expiry.ts 2
SH-132 Q3 seat-hold/holds.ts, seat-hold/expiry.ts 8
SH-133 Q3 seat-hold/release-rules.ts 3
SH-134 Q3 seat-hold/expiry.ts 3
SH-135 Q3 seat-hold/holds.ts 6
SH-136 Q3 seat-hold/release-rules.ts 2
SH-137 Q3 seat-hold/holds.ts, seat-hold/release-rules.ts 9
SH-138 Q3 seat-hold/expiry.ts 4
SH-139 Q3 seat-hold/holds.ts, seat-hold/expiry.ts 8
What happened to the median change lead time of the team's seat-hold changes?
Question 3 of 8
A second ticketing team keeps a standing 25% share of its capacity for its top hotspot, venue/seat-map.ts, from sprint 1; nothing else draws on the share. The refactor is estimated at 32 engineer-days. Sprints are two weeks of 10 working days. The team has 4 engineers in sprints 1 and 2, and 8 from sprint 3 on.
In which sprint does the refactor finish?
Question 4 of 8
In a mock interview you proposed paying down a ticketing platform's top hotspot, seat-hold/holds.ts, with a refactor estimated at 36 engineer-days. The interviewer follows up: "The team is 6 engineers in two-week sprints of 10 working days. The product lead will not stop feature work in any sprint, and the season opens after sprint 4: by then the refactor must be finished and features must have had at least 180 engineer-days. What do you propose? Three candidates answered:
Whose answer survives the follow-up?
Question 5 of 8
A mock interview turns to debt work you have seen funded.
How do you answer, in at most 150 words and with one project you led or saw (a ticketing example will do), the interviewer's question "How did you get leadership to fund a debt project, and how did you show it worked"?
Question 6 of 8
A hiring manager wants an answer you could say aloud.
What do you write, in at most 120 words, to their question "In a large codebase, how do you decide which tech debt to pay first"?
Question 7 of 8
A payments-ledger team builds a hotspot list over the last six months of its repository, with lines of code as the complexity measure. Today is 15 October 2026.
file lines of code commits, last 6 months of them since 15 Sep
posting/rules.ts 1,500 30 2
settlement/batch.ts 2,600 12 6
payouts/schedule.ts 500 45 20
fx/rates.ts 300 70 12
fx/round.ts 40 16 3
Which file tops the list?
Question 8 of 8
A chat app's team builds a hotspot list over the last six months of its repository, with lines of code as the complexity measure. Today is 15 October 2026. On 2 July one commit, style: apply prettier 3, reformatted every file in the repository; both commit columns below include it.
file lines of code commits, last 6 months of them since 2 Jul
presence/tracker.ts 1,100 7 2
chat/send.ts 250 30 10
media/thumbs.ts 500 12 9
rooms/members.ts 200 20 8
Which file should top the list?
Go deeper
Pro tip
- Count commits per file with plain git before reaching for a tool.
When: in the first minutes of a debt discussion, to see where change goes; it gives one axis of the matrix, not the list (step 2).git log --since=2025-10-15 --format= --name-only | sort | uniq -c | sort -rn | head -3
tip 1 git log --since=2025-10-15 --format= --name-only, counted: 88 proctor/flags.ts · 41 grading/rules.ts · 35 candidate/autosave.ts · same counts as the tool's churn column
Source
- Adam Tornhill, Your Code as a Crime Scene, 2nd ed. (Pragmatic Bookshelf, 2024) (the row's first source): a book, not fetched; it keys nothing here. The hotspot idea (change frequency from version control times a complexity measure, with lines of code as a usable first measure) is taught from it in steps 2–5 and executed with the tool below.
- Martin Fowler, "Technical Debt Quadrant" (the row's second source): https://martinfowler.com/bliki/TechnicalDebtQuadrant.html · fetched 2026-10-15 with
curl. The quadrant and the interest metaphor belong to the prerequisite; this lesson quotes nothing from it. Valid as of 2026-10-15. code-complexity4.4.4 (the tool every hotspot item runs): README at the v4.4.4 tag, https://github.com/simonrenoult/code-complexity/blob/v4.4.4/README.md · fetched 2026-10-15: "Measure the churn/complexity score. Higher scores reveal hotspots where refactorings should happen." (its opening blockquote) and "lines of code count (all languages)" (steps 3–4); its options--since,--filterand--sortare the ones the scripts pass. Its installed source (dist/src/lib/statistics/statistic.js) computesthis.score = this.churn * this.complexity, and its churn adds one per file name ingit log --name-only, so one per commit. Valid as of 2026-10-15.- Git, "git-log" documentation: https://git-scm.com/docs/git-log · fetched 2026-10-15:
--since=<date>("Show commits more recent than <date>."),--name-only("Show only the name of each changed file in the post-image tree.") and--numstat("shows number of added and deleted lines in decimal notation"), as the scripts and tip 1 use them. Valid as of 2026-10-15.
Related lessons
Checked against the tools and docs as of 2026-10-15.