How to track technical debt: a tech debt register template that gets used
A technical debt register template, a scoring method based on interest and cost of delay, GitHub labels and an issue form, and ways to get time to pay it down.
The KnowWhyy team · · 8 min read
Every team has technical debt. Most teams can't tell you where it is. It lives in TODO comments, in a senior engineer's head, in a Slack thread from last spring and in the groan someone makes when a ticket touches the billing module. When it's time to plan, nobody can compare "the billing module is painful" against a feature with a revenue number attached, so the debt loses every time.
A tech debt register fixes the first half of that problem. It's a single list of known debt, with enough structure to compare items and enough context to remember why each one exists. This guide gives you a template, a scoring method you can explain to a product manager, a GitHub setup with labels and an issue form, and some ways to actually get time to pay debt down.
What technical debt means (and where the term comes from)
Ward Cunningham introduced the metaphor in a 1992 OOPSLA experience report about a portfolio management system. His point was narrow and useful: "Shipping first time code is like going into debt." A little debt speeds you up, as long as you pay it back with a rewrite once you understand the problem better. The danger is never paying it back.
Interest is the extra effort the debt adds to each change. In Fowler's example, a feature that would take four days in a clean module takes six because of a confusing structure. The two extra days are interest.
Principal is the cost of fixing the underlying problem. If cleanup takes five days, it pays off only if you'll make enough changes in that area afterwards.
Ask your first question in minutes.
Create a workspace, connect a tool and choose what gets indexed. The free plan includes GitHub, Slack and Notion.
Fowler also notes where the metaphor breaks: you pay interest only when you touch the code. A messy module that nobody changes costs almost nothing. That single idea should drive most of your prioritization.
You knowingly took a shortcut to hit a date, and planned to pay it back
You learned, after building it, what the design should have been
Reckless
You knew better but skipped good practice to go faster
The team didn't know the practice and created a mess without realizing
This is worth a field in your register, for two reasons. Deliberate debt should come with a reason and a payback trigger, written at the time. If you're seeing lots of reckless debt, the fix is training and review, not a cleanup sprint. Fowler argues prudent, inadvertent debt is inevitable even for excellent teams, because you learn as you build. So don't treat a growing register as a failure. Treat an unexplained one as a failure.
The tech debt register template
You can keep this in a spreadsheet, a Notion database or GitHub issues. The fields matter more than the tool.
Shipped tax support before the Q3 launch; PR #1201
Interest
What it costs each time someone touches it
Every pricing change needs three edits; two bugs in Q2
Change frequency
How often this area changes
14 commits in the last 6 months
Principal
Rough effort to fix
3 to 5 days
Risk
What happens if it's never fixed
Mismatched totals on invoices
Payback trigger
When this must be fixed
Before multi-currency work starts
Owner
One person
@maria
Status
Open, Scheduled, In progress, Paid, Accepted
Open
Last reviewed
Date
2026-10-14
Two statuses deserve explanation. Paid means fixed. Accepted means you've decided to live with it, usually because the code rarely changes. Accepted items stay in the register so nobody re-raises them every quarter, the same way a decision log stops teams from relitigating decisions.
Record why the debt was taken on
The "why we took it on" field is the one most registers skip, and the one people need most a year later. Without it, a deliberate trade-off looks like carelessness, and someone either "fixes" it at the wrong time or is afraid to touch it at all.
Write it when the debt is created, in the PR that creates it. A short block in the PR description is enough:
### Debt taken on
What: Invoice totals are calculated in the API, the PDF renderer and the
export job instead of one shared function.
Why: Tax support had to ship before the Q3 launch; consolidating needed
changes to the export job we couldn't test in time.
Payback trigger: Before any multi-currency work.
Tracking issue: #1482
Then link the code to the issue so the next reader finds the context from the file itself:
// TODO(#1482): totals duplicated in pdf/render.ts and jobs/export.ts.
// Consolidate before multi-currency. See issue for why.
A TODO with an issue number is searchable and has an owner. A bare TODO is a wish. If you need to reconstruct the reason for older debt, finding out why code was written walks through git blame, PR history and Slack.
How to score and prioritize debt
You can't measure productivity precisely, and Fowler says as much: estimates of interest and cleanup cost are rough. So use coarse scales and compare items to each other, not to an absolute standard.
Step 1: score interest, frequency, risk and principal from 1 to 5
Score
Interest (pain per change)
Frequency (how often touched)
Risk (if never fixed)
Principal (effort)
1
Barely noticeable
Once a year or less
Cosmetic
Under a day
3
Adds hours to a change
Monthly
Bugs customers notice
About a week
5
Adds days, or blocks work
Weekly or more
Data loss, security, outage
A month or more
You can get a real number for frequency from git instead of guessing. This lists the most-changed files in the last six months:
Debt in files at the top of that list is costing you interest constantly. Debt in files that never appear is a candidate for Accepted.
Step 2: combine them into a priority
A simple formula that follows the interest-versus-principal logic:
priority = (interest × frequency + risk) ÷ principal
This is a heuristic, not a law. It rewards fixing cheap debt in busy code, and it pushes expensive cleanups of quiet code to the bottom, which is where Fowler's reasoning puts them.
Step 3: use cost of delay for the big items
For large items that compete directly with features, borrow from product prioritization. Cost of Delay describes what you lose by doing something later, combining value and urgency. Dividing it by duration (CD3, "Cost of Delay Divided by Duration") tells you which item gives you the most value per week of work.
For debt, the cost of delay is the interest you keep paying, plus any deadline the debt threatens. "Each month we don't fix this, the four planned pricing changes each take two extra days, and multi-currency can't start" is a cost of delay a product manager can weigh against a feature.
Track it in GitHub with labels and an issue form
If your team already lives in GitHub, keep the register there. Issues give you owners, links to PRs and a history of the discussion.
Create a small set of labels with the GitHub CLI's gh label create:
gh label create "tech-debt" --color 5319E7 --description "Known technical debt"
gh label create "debt: high-interest" --color B60205 --description "Slows frequent changes"
gh label create "debt: accepted" --color C5DEF5 --description "Decided to live with it"
gh label create "debt: deliberate" --color FBCA04 --description "Taken on knowingly, with a payback trigger"
Add an issue form so every debt issue captures the same fields. Put this at .github/ISSUE_TEMPLATE/tech-debt.yml. The issue form syntax requires name, description and body. Note that labels listed in the form are only applied if they already exist in the repository, so create them first.
name: Technical debt
description: Record a piece of known technical debt.
title: "[Debt]: "
labels: ["tech-debt"]
body:
- type: textarea
id: problem
attributes:
label: What is the debt?
description: Describe the problem, not the fix. Include file paths.
validations:
required: true
- type: textarea
id: why
attributes:
label: Why was it taken on?
description: The trade-off at the time, with links to the PR, decision or thread.
validations:
required: true
- type: textarea
id: interest
attributes:
label: What does it cost us?
description: Extra time per change, bugs, incidents, blocked work.
validations:
required: true
- type: dropdown
id: quadrant
attributes:
label: Quadrant
options:
- Prudent, deliberate
- Prudent, inadvertent
- Reckless, deliberate
- Reckless, inadvertent
- type: input
id: trigger
attributes:
label: Payback trigger
description: The event or date by which this should be fixed.
For the scores, add a GitHub Project with custom number and single select fields for interest, frequency, risk and principal. Then sort by priority in a table view. Useful searches:
A register nobody acts on is a list of regrets. Some approaches that work, roughly in order of how little negotiation they need:
Pay it down where you're already working. Fowler recommends paying principal gradually: when a feature touches crufty code, spend some of that feature's time cleaning it. This naturally targets the busiest code, which is where interest is highest. Include the cleanup in the estimate rather than asking for it separately.
Fix before you build. Use payback triggers. If a big feature is about to land in an area with high-interest debt, schedule the debt work as the first ticket of that project, framed as making the feature cheaper.
Reserve a steady share of capacity. Some teams set aside a fixed part of each sprint or cycle for register items. Pick a share your team can sustain and protect it, rather than running occasional cleanup sprints that get cancelled.
Speak in features and dates. Fowler notes the debt metaphor is useful because non-technical people understand it. "Fixing #1482 takes four days and saves two days on each of the next four pricing changes" lands better than "the code is bad".
Show what's been paid. Close the loop in your weekly status report or sprint review: which items were paid, and what got faster as a result.
Review the register once a month. Re-score anything whose area got busier, mark quiet items Accepted, and close what's been fixed.
Where KnowWhyy fits
The hardest field to fill in is "why we took it on", especially for debt that's years old. KnowWhyy indexes the GitHub repositories, Slack channels and Notion pages you choose, including PRs, issues and their discussions, though not the source code itself. You can ask in the web app, for example "why are invoice totals calculated in the export job?", and get an answer that cites the specific PRs and threads. If the sources don't say, it tells you instead of guessing. It doesn't track or score debt for you. It helps you recover the context so the register entry is accurate. See GitHub history search, or start free.
The short version
Technical debt is Ward Cunningham's metaphor: interest is the extra effort per change, principal is the cost to fix, and you only pay interest on code you touch.
Keep one register with an owner, interest, change frequency, principal, risk, payback trigger and, most importantly, why the debt was taken on.
Score on coarse 1-to-5 scales, use git history for frequency, and prioritize cheap fixes in busy code. Use cost of delay for big items.
In GitHub, use a tech-debt label, an issue form for consistent fields, TODO(#issue) comments and a Project for scores.
Get time by paying down debt inside feature work, fixing before building in risky areas and protecting a steady share of capacity.