Skip to content
academia.sh

Lesson 05 / 11

Technical Debt Management

Separating and measuring deliberate from inadvertent debt: debt items are split into two classes, debt interest is modeled as the difference in work the same change causes in an indebted module versus a debt-free one, yearly interest per item and the rate of extra work per module are computed, the delay before inadvertent debt is noticed is measured as a function of a repeat threshold, and the interest paid unnoticed across that delay is counted.

Contents

A risk register tracks something not yet realized: an event with a likelihood that has not happened yet. Next to it stands a second record, and that one tracks something already realized. When a decision is made, sometimes an incomplete solution is chosen deliberately; the gap stays in the system and produces extra work with every change. The name for this gap is technical debt.

How technical debt gets defined as a metric was addressed in the Quality Metrics lesson; it is not repeated here. This lesson has two questions. First: once debt items are split into two classes — taken knowingly and taken without realizing it — does that split produce a measurable difference. Second: what is debt interest, and how is it counted.

Two Classes and Interest

Deliberate debt is a gap chosen knowingly at decision time: to ship faster, or because the right solution is not yet known. It can be recorded, because the person who chose it knows they chose it. Inadvertent debt is not chosen; it is understood only later that an assumption was wrong, or that a copy landed in two places. It cannot be recorded, because no one is aware of it while the record is being written.

The way to measure interest reduces to a single question: how many more minutes does the same change take in the indebted module than in a debt-free one. This does not say the debt is “bad” — it says how bad. In the model below, five change types, their base durations, and their yearly counts are the model’s inputs (DR14). Every debt item carries which change types it affects and how many extra minutes it adds for that type. The scenario is fictional.

// debt/interest.mjs — debt items are split into two classes, interest is measured as extra work per change
// Change types, base durations, and yearly counts are the model's input (DR14); the scenario is fictional.
export const CHANGE_TYPES = [
  { type: "rule-change", base: 120, yearly: 12 },
  { type: "new-field", base: 90, yearly: 8 },
  { type: "new-screen", base: 240, yearly: 6 },
  { type: "bug-fix", base: 60, yearly: 30 },
  { type: "performance-tuning", base: 150, yearly: 4 },
];

// extraWork: how many extra minutes the same change takes in the indebted module vs. the debt-free module
export const ITEMS = [
  { name: "rule-copy", class: "deliberate", module: "loan", extraWork: 35,
    affects: ["rule-change", "bug-fix"] },
  { name: "manual-field-mapping", class: "deliberate", module: "catalog-mapping", extraWork: 45,
    affects: ["new-field", "bug-fix"] },
  { name: "temp-membership-table", class: "deliberate", module: "membership", extraWork: 20,
    affects: ["new-field", "new-screen"] },
  { name: "fixed-report-query", class: "deliberate", module: "reporting", extraWork: 30,
    affects: ["new-screen"] },
  { name: "untested-penalty-branch", class: "inadvertent", module: "loan", extraWork: 40,
    affects: ["rule-change", "bug-fix", "performance-tuning"] },
  { name: "date-format-in-two-places", class: "inadvertent", module: "catalog-mapping", extraWork: 25,
    affects: ["new-field", "bug-fix"] },
  { name: "hidden-branch-assumption", class: "inadvertent", module: "membership", extraWork: 55,
    affects: ["new-screen", "bug-fix"] },
  { name: "duplicated-authorization-check", class: "inadvertent", module: "reporting", extraWork: 35,
    affects: ["new-screen", "new-field", "bug-fix"] },
  { name: "manual-cache-cleanup", class: "inadvertent", module: "loan", extraWork: 15,
    affects: ["performance-tuning", "bug-fix"] },
];

export const MODULES = [...new Set(ITEMS.map((k) => k.module))];
export const yearly = (type) => CHANGE_TYPES.find((d) => d.type === type).yearly;
export const itemInterest = (k) => k.extraWork * k.affects.reduce((t, type) => t + yearly(type), 0);
export const baseWork = CHANGE_TYPES.reduce((t, d) => t + d.base * d.yearly, 0);  // per module

if (import.meta.url.endsWith(process.argv[1].split("/").pop())) {
  const deliberate = ITEMS.filter((k) => k.class === "deliberate");
  console.log(`${ITEMS.length} debt items: ${deliberate.length} deliberate (recorded), ` +
    `${ITEMS.length - deliberate.length} inadvertent (unrecorded)`);
  console.log(`${CHANGE_TYPES.reduce((t, d) => t + d.yearly, 0)} changes a year per module, ` +
    `${baseWork} min in a debt-free module\n`);

  console.log("yearly interest per item (extra work per change x yearly change count):");
  for (const k of [...ITEMS].sort((a, b) => itemInterest(b) - itemInterest(a)))
    console.log(`  ${k.name.padEnd(31)} ${k.class.padEnd(11)} ${k.module.padEnd(15)} ` +
      `${String(k.extraWork).padStart(2)} min x ${String(k.affects.reduce((t, type) => t + yearly(type), 0)).padStart(2)} ` +
      `changes = ${String(itemInterest(k)).padStart(4)} min/yr`);

  console.log("\ntotal interest per class:");
  for (const s of ["deliberate", "inadvertent"]) {
    const g = ITEMS.filter((k) => k.class === s);
    const f = g.reduce((t, k) => t + itemInterest(k), 0);
    console.log(`  ${s.padEnd(11)} ${g.length} items, ${f} min/yr, per item ${(f / g.length).toFixed(0)} min`);
  }

  console.log("\nyearly work per module (base + interest):");
  for (const m of MODULES) {
    const f = ITEMS.filter((k) => k.module === m).reduce((t, k) => t + itemInterest(k), 0);
    console.log(`  ${m.padEnd(15)} base ${baseWork.toFixed(0)} + interest ${String(f).padStart(4)} = ` +
      `${(baseWork + f).toFixed(0)} min  (${(100 * f / baseWork).toFixed(1)}% extra)`);
  }
}
9 debt items: 4 deliberate (recorded), 5 inadvertent (unrecorded)
60 changes a year per module, 6000 min in a debt-free module

yearly interest per item (extra work per change x yearly change count):
  hidden-branch-assumption        inadvertent membership      55 min x 36 changes = 1980 min/yr
  untested-penalty-branch         inadvertent loan            40 min x 46 changes = 1840 min/yr
  manual-field-mapping            deliberate  catalog-mapping 45 min x 38 changes = 1710 min/yr
  duplicated-authorization-check  inadvertent reporting       35 min x 44 changes = 1540 min/yr
  rule-copy                       deliberate  loan            35 min x 42 changes = 1470 min/yr
  date-format-in-two-places       inadvertent catalog-mapping 25 min x 38 changes =  950 min/yr
  manual-cache-cleanup            inadvertent loan            15 min x 34 changes =  510 min/yr
  temp-membership-table           deliberate  membership      20 min x 14 changes =  280 min/yr
  fixed-report-query              deliberate  reporting       30 min x  6 changes =  180 min/yr

total interest per class:
  deliberate  4 items, 3640 min/yr, per item 910 min
  inadvertent 5 items, 6820 min/yr, per item 1364 min

yearly work per module (base + interest):
  loan            base 6000 + interest 3820 = 9820 min  (63.7% extra)
  catalog-mapping base 6000 + interest 2660 = 8660 min  (44.3% extra)
  membership      base 6000 + interest 2260 = 8260 min  (37.7% extra)
  reporting       base 6000 + interest 1720 = 7720 min  (28.7% extra)

The item table’s first piece of information is in the ranking: the highest-interest item is not the one that adds the most work per change. temp-membership-table adds 20 minutes per change and costs 280 minutes a year; manual-cache-cleanup adds 15 minutes and costs 510. The difference is in how many changes the item touches. A debt item’s weight is measured not by how ugly it is, but by how often it gets set off.

The class table gives the second piece of information. Five inadvertent items cost 6820 minutes a year, four deliberate items 3640; 1364 minutes per item against 910. Inadvertent debt is more expensive per item. The reason is structural, not moral: deliberate debt is taken as part of a decision, and its scope is thought through when it is taken, while inadvertent debt grows without anyone watching. The measured difference is the numeric counterpart of these two states not being the same thing.

The module table compares interest against base work. In the loan module, the same yearly set of changes takes 9820 minutes, against 6000 in a debt-free module — 63.7 percent extra work. In the reporting module, the same ratio is 28.7 percent. It cannot be said the two modules do different work — exactly the same set of changes was run. The entire gap between them is debt.

The Delay Before Noticing

Deliberate debt has zero delay before entering the record: it is known the moment it is chosen. Inadvertent debt is noticed at some point, and its interest keeps being paid until then. Noticing does not happen through a single painful change — it happens through the same extra work repeating: someone sees a pattern once they get stuck in the same place on two different tasks. In the model, an item is noticed once it has produced the same extra work a threshold number of times (DR15). This means the delay is tied not to the item’s size, but to how often the changes it touches occur.

// debt/delay.mjs — the delay before inadvertent debt is noticed, and the interest paid unnoticed until then
import { ITEMS, yearly, itemInterest } from "./interest.mjs";

// An item is noticed once the same extra work has shown up THRESHOLD times; changes are spread
// evenly across the year. Both are the model's input (DR15).
const THRESHOLD = [8, 24, 48];
const MONTHS = 12;
const affected = (k) => k.affects.reduce((t, type) => t + yearly(type), 0);
const delay = (k, threshold) => (MONTHS * threshold) / affected(k);
const inadvertent = ITEMS.filter((k) => k.class === "inadvertent");

console.log(`${inadvertent.length} inadvertent items; deliberate debt has zero delay because it is written into the record`);
console.log("threshold  average delay  unnoticed at month 12  interest paid unnoticed  share of yearly interest");
const yearlyInterest = inadvertent.reduce((t, k) => t + itemInterest(k), 0);
for (const threshold of THRESHOLD) {
  const delays = inadvertent.map((k) => delay(k, threshold));
  const paid = inadvertent.reduce((t, k) => t + threshold * k.extraWork, 0);
  console.log(`  ${String(threshold).padStart(2)}  ${(delays.reduce((a, b) => a + b, 0) / delays.length).toFixed(1).padStart(13)} mo ` +
    `${String(delays.filter((x) => x > MONTHS).length).padStart(20)}  ${String(paid).padStart(21)} min ` +
    `${((100 * paid / yearlyInterest).toFixed(0) + "%").padStart(15)}`);
}

console.log(`\nper item (threshold 24; yearly inadvertent interest ${yearlyInterest} min):`);
for (const k of [...inadvertent].sort((a, b) => delay(a, 24) - delay(b, 24)))
  console.log(`  ${k.name.padEnd(31)} ${String(affected(k)).padStart(2)} changes/yr, ` +
    `${delay(k, 24).toFixed(1).padStart(4)} mo until noticed, ${24 * k.extraWork} min paid until then`);

// Question set: 9 items x 4 questions; based on which items are in the record by month 12
const QUESTIONS = ["B1 was this gap chosen deliberately", "B2 when did it enter",
  "B3 what was the payback condition", "B4 how much extra work does it cause per change"];
console.log(`\nquestion set at month 12 (${ITEMS.length} items x ${QUESTIONS.length} questions = ` +
  `${ITEMS.length * QUESTIONS.length} answers):`);
// A deliberate item answers all four questions; a noticed inadvertent item answers B1 and B4,
// B2 and B3 stay missing; a never-noticed item answers B1 wrong, the rest is missing.
const DELIBERATE_COUNT = ITEMS.length - inadvertent.length;
for (const threshold of THRESHOLD) {
  const found = inadvertent.filter((k) => delay(k, threshold) <= MONTHS).length;
  const outside = inadvertent.length - found;
  console.log(`  threshold ${String(threshold).padStart(2)}: in record ${DELIBERATE_COUNT + found}/${ITEMS.length} items -> ` +
    `correct ${4 * DELIBERATE_COUNT + 2 * found}, missing ${2 * found + 3 * outside}, wrong ${outside}`);
}
5 inadvertent items; deliberate debt has zero delay because it is written into the record
threshold  average delay  unnoticed at month 12  interest paid unnoticed  share of yearly interest
   8            2.5 mo                    0                   1360 min             20%
  24            7.4 mo                    0                   4080 min             60%
  48           14.7 mo                    5                   8160 min            120%

per item (threshold 24; yearly inadvertent interest 6820 min):
  untested-penalty-branch         46 changes/yr,  6.3 mo until noticed, 960 min paid until then
  duplicated-authorization-check  44 changes/yr,  6.5 mo until noticed, 840 min paid until then
  date-format-in-two-places       38 changes/yr,  7.6 mo until noticed, 600 min paid until then
  hidden-branch-assumption        36 changes/yr,  8.0 mo until noticed, 1320 min paid until then
  manual-cache-cleanup            34 changes/yr,  8.5 mo until noticed, 360 min paid until then

question set at month 12 (9 items x 4 questions = 36 answers):
  threshold  8: in record 9/9 items -> correct 26, missing 10, wrong 0
  threshold 24: in record 9/9 items -> correct 26, missing 10, wrong 0
  threshold 48: in record 4/9 items -> correct 16, missing 15, wrong 5

The threshold table gives the cost of the delay. Debt noticed after eight repeats enters the record after an average of 2.5 months and extracts 1360 minutes of payment until then; at twenty-four repeats, 7.4 months and 4080 minutes. A pattern requiring forty-eight repeats is not noticed at all within a year, and the interest paid rises to 120 percent of the yearly inadvertent interest — meaning no one knows what is slowing things down for more than a year. The difference across these three rows comes down to one thing: how long it takes for the extra work to be seen as a pattern.

The item table shows the outcome of that link, and it runs against intuition. hidden-branch-assumption is the highest-interest item — 1980 minutes a year — but it is one of the latest to be noticed, at 8.0 months. untested-penalty-branch carries less interest and is noticed at 6.3 months. The reason is the model’s rule: noticing depends not on how much each occurrence hurts, but on how many times it repeats. A debt that lands a few heavy hits stays invisible longer than one that lands frequent light ones. If the noticing rule were tied to size, the order would reverse; the measured result depends on the rule itself, and the result cannot be interpreted without the rule being written down.

The question set table comes out the same at two thresholds and splits at the third. A noticed inadvertent debt item answers two of its four questions: it can be said that it was not chosen deliberately, and the extra work it causes per change can be measured. When it entered and what its payback condition was stay unanswered, because that information was never written when the item entered and cannot be produced afterward. This is the one real advantage deliberate debt has over inadvertent debt: it pays the same interest, but it answers all four questions.

Summary

  • A debt item’s yearly interest is the product of the work it adds per change and the number of changes it touches; the highest-interest item is not the one adding the most work per change.
  • Five inadvertent items cost 6820 minutes a year, four deliberate items 3640; 1364 minutes per item against 910, meaning inadvertent debt is more expensive per item.
  • The same yearly set of changes takes 9820 minutes in the loan module against 6000 in a debt-free module — 63.7 percent extra work; in the reporting module the same ratio is 28.7 percent.
  • The delay before inadvertent debt is noticed depends on the repeat threshold: 2.5 months and 1360 minutes at eight repeats, 7.4 months and 4080 minutes at twenty-four, and at forty-eight it is not noticed at all within a year, with the interest paid rising to 120 percent of the yearly inadvertent interest.
  • The order of noticing is not the same as the order of interest; the highest-interest item is among the latest noticed, at 8.0 months, because in this model noticing is tied to repetition, not to size.

Next Step

Everything written across this topic gathered around a single decision. The decision record holds the decision’s rationale, the proposal process holds how the decision was reached, the trade-off analysis holds what was traded for what, the risk register holds what might not hold, the debt record holds what was deliberately left incomplete. Five record formats, and a measured question set for each.

All five share one limit: each is the record of a single decision. Someone new to the library network’s system, reading every one of these records, learns the rationale behind forty decisions but does not learn what the system looks like. Which parts exist, which depends on which, where data flows from and to, which part sits on which machine — none of these questions is answered in the record of any single decision, because none of them belongs to a single decision. Stacking the records on top of one another gives no picture either; the sum of forty records is not a map of the system. The next topic takes on that gap: it measures what a description showing the system as a whole is, how many distinct views it needs, and which question each view answers.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close