How to Map Technical Debt With Hotspots and Dependency Data

A debt list built from opinions never gets funded. A map built from churn, complexity and dependencies shows where debt costs you, and what to fix first.

By The OpenVisio Team · · 11 min read

Most technical debt lists are a spreadsheet of complaints. Someone dislikes the ORM layer, someone else wants to rewrite the frontend state management, and a third person has a long-standing grudge against the build scripts. Each item may be valid, but the list has no way to say which debt is actually costing the team, so it never gets prioritized and never gets funded.

Debt only matters where it slows you down or causes failures, and that happens in code people actually change. A messy module nobody touches is a museum piece. A messy module that every feature has to pass through is a tax on every sprint. Mapping technical debt means finding that second kind with data: how often code changes, how complex it is, how many other parts depend on it, and how often it is involved in bugs. This guide walks through building that map with standard tools and turning it into a short, defensible plan.

Key takeaways

  • Debt costs you in proportion to how often you touch it. Start from change frequency, not code smell.
  • A hotspot is code that is both frequently changed and hard to work with. Churn times complexity is a simple, useful proxy.
  • Add dependency data to see blast radius: a hotspot many modules depend on is riskier and higher leverage than a leaf.
  • Bug-fix history and change coupling expose debt that complexity metrics miss.
  • Present debt as a short ranked list with evidence and a proposed next step, not a catalog of everything wrong.
  • Re-run the analysis each quarter. A debt map is only useful if it shows movement.

Why opinion-based debt lists fail

Engineers are good at spotting code that is ugly. They are less reliable at estimating which ugliness matters, because everyone's view is biased toward the code they personally work in. The result is a list where the loudest complaints rank highest. When engineering leadership asks for prioritization, there is no shared basis for comparison, and product managers reasonably push back on spending a sprint on something that has no visible cost.

Data does not remove judgment, but it gives the conversation a common starting point. "This file was changed in a large share of last quarter's feature PRs, has the highest complexity in the service, and shows up in most of the incident follow-ups" is a much stronger case than "this file is gross".

Step 1: Measure churn

Churn is how often a file changes. Git gives you this directly.

# Files changed most often in the last 12 months
git log --since="12 months ago" --no-merges --format= --name-only \
  | grep -v '^$' \
  | sort | uniq -c | sort -rn | head -50

Filter out noise before trusting the list. Lockfiles, generated code, changelogs, snapshot files and translation files churn constantly without being debt.

git log --since="12 months ago" --no-merges --format= --name-only \
  | grep -v '^$' \
  | grep -Ev '(package-lock\.json|yarn\.lock|\.snap$|CHANGELOG|/generated/|\.po$)' \
  | sort | uniq -c | sort -rn | head -50

You can also measure churn as lines added and removed, which separates a file that gets one-line config tweaks from one that gets rewritten constantly.

git log --since="12 months ago" --no-merges --numstat --format= \
  | awk '{ add[$3]+=$1; del[$3]+=$2 } END { for (f in add) print add[f]+del[f], f }' \
  | sort -rn | head -50

Step 2: Measure complexity

Complexity estimates how hard a file is to change safely. There are several reasonable measures, and the exact one matters less than applying it consistently.

LanguageToolExample
Many languageslizard (cyclomatic complexity per function)lizard src/ --csv > complexity.csv
Many languagesscc (lines and a complexity estimate per file)scc --by-file --format csv src/
Pythonradonradon cc -s -a src/
JavaScript and TypeScriptESLint complexity rule"complexity": ["warn", 15]
Gogocyclogocyclo -over 15 ./...

If you do not want to install anything, lines of code per file is a crude but surprisingly serviceable proxy. Long files that change often are a reliable smell.

Step 3: Combine them into hotspots

A hotspot is a file in the top tier of both churn and complexity. You can join the two lists with a short script.

import csv, subprocess, collections

log = subprocess.run(
    ['git', 'log', '--since=12 months ago', '--no-merges', '--format=', '--name-only'],
    capture_output=True, text=True,
).stdout.split()
churn = collections.Counter(log)

# scc --by-file --format csv src/ > scc.csv
rows = []
with open('scc.csv') as f:
    for r in csv.DictReader(f):
        path = r['Location']
        complexity = int(r['Complexity'])
        rows.append((churn.get(path, 0) * complexity, churn.get(path, 0), complexity, path))

for score, c, cx, path in sorted(rows, reverse=True)[:25]:
    print(f'{score:>8} churn={c:<4} complexity={cx:<5} {path}')

Column names vary between tool versions, so check the header of your CSV. The output is a ranked list that usually confirms some suspicions and surprises the team with others. The surprises are where the value is. The deeper treatment of this technique is in code hotspots and churn analysis, and Adam Tornhill's open source code-maat tool implements several related analyses on git logs if you want more than a script.

Step 4: Add dependency data for blast radius

Churn and complexity tell you where work is painful. Dependency data tells you how far problems spread. A complex, frequently changed file that sits at the center of the import graph affects every module that imports it. The same file as a leaf affects only itself.

Count dependents for your top hotspots. For JavaScript and TypeScript, madge can produce the graph:

npx madge --json src/ > deps.json
# Then count how many files import each hotspot
node -e '
const g = require("./deps.json")
const counts = {}
for (const [file, imports] of Object.entries(g))
  for (const i of imports) counts[i] = (counts[i] || 0) + 1
Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20)
  .forEach(([f, n]) => console.log(n, f))
'

For Python, pydeps or import-linter can describe the import graph; for Go, go list -deps and go mod graph give package-level relationships. A code graph tool that indexes the whole repository will give you dependents for any file in one query. If you are new to reading these graphs, see dependency graphs explained.

High-dependent hotspots are higher leverage: improving them helps many callers. They are also higher risk to refactor, so they need the techniques in safe refactoring in large codebases.

Step 5: Add bug and coupling signals

Two more signals catch debt that metrics miss.

Bug-fix density. If your commit messages or PR titles follow a convention, count fixes per file.

git log --since="12 months ago" --no-merges -i --grep='^fix' --format= --name-only \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -30

A file that shows up in many fix commits is costing you in reliability, not just velocity.

Change coupling. Files that always change together, especially across module boundaries, often indicate a hidden dependency or a leaky abstraction. A quick way to look for pairs:

git log --since="12 months ago" --no-merges --format='--%h' --name-only \
  | awk '/^--/ { if (n) for (i in f) for (j in f) if (i < j) print i, j; delete f; n=0; next }
         NF { f[$0]=1; n++ }' \
  | sort | uniq -c | sort -rn | head -30

If api/orders.ts and billing/ledger.ts change together in most commits, every order change secretly requires a billing change. That is debt worth naming, even if neither file looks complex.

Step 6: Validate with the people who work there

Data finds candidates. People confirm them. Take the top ten files to the engineers who change them most and ask three questions:

  1. When you change this, what slows you down?
  2. What has broken here that should not have?
  3. If you had a week, what is the smallest change that would make this noticeably easier?

Some hotspots will turn out to be fine: a central router that changes often because every feature adds a route, with low real complexity per change. Drop those. Others will come with a specific, cheap fix the team has wanted for months. Those go to the top.

Step 7: Present a ranked, evidence-based plan

The output should fit on one page. For each item include the evidence, the cost, and a bounded next step.

RankAreaEvidenceCost todayNext stepSize
1billing/ledger.tsTop churn, top complexity, many dependents, frequent fix commitsMost billing features touch it; recurring incidentsExtract posting rules into a tested pure module1 sprint
2Order and ledger couplingChange together in most commitsEvery order change needs a billing reviewerIntroduce an event between orders and billing2 sprints
3search/indexer.pyHigh churn, long functionsSlow reviews, frequent reworkCharacterization tests, then split by document type3 days

Note that each next step is small. "Rewrite billing" is not a plan. "Extract posting rules into a tested pure module" is, and it can be scheduled alongside feature work. Record significant decisions in an ADR so the reasoning survives.

Step 8: Track movement

Re-run the same scripts each quarter and compare. You want to see the top hotspots drop in complexity or churn, fewer fix commits in the areas you invested in, and coupling pairs that disappear. If an item has not moved after two quarters, either the next step was wrong or it never got time. Both are worth discussing openly.

Keep the scripts in the repository, for example under scripts/debt/, so the analysis is reproducible and anyone can run it.

Making the map visible

Numbers in a terminal convince engineers. A picture convinces everyone else. A treemap or architecture view with hotspots highlighted makes it obvious where the trouble is concentrated. OpenVisio maps a repository in the browser, so the Explore view shows where a hotspot sits in the architecture and what depends on it, and coding agents can request a churn and centrality ranking through its get_hotspots MCP tool when planning refactors. Use whatever renders the map for your audience; the important part is that the map is generated from the code and the history, not drawn by hand.

Technical debt becomes manageable when it is specific, ranked by real cost, and paired with small next steps. Build the map from churn, complexity, dependencies and bug history, check it with the people who do the work, and revisit it on a schedule.

technical debthotspotscode churnrefactoringengineering management

See it on your own repo

Paste a GitHub URL and get the day-one walkthrough — free, in your browser.

Index a repo

Keep reading