Lesson 05 / 10
Engineering Practices
Measuring feedback delay: how many steps and how many hours separate the step a defect is born in from the step it is found in, and how this delay, rework, and total work effort — including pair programming's two-person cost — change as integration frequency is scanned from two hours to forty hours and pairing ratio from zero to one.
Contents
The previous four lessons always took one number as an assumption: when a defect is found, there is a return trip. How many items came back and what they cost was counted, but when the defect is found was read off the shape of the process every single time. As this lesson closes the topic, it measures that number directly: feedback delay, the distance between the step a defect is born in and the step it is found in.
The distance is measured in two separate units, and they do not say the same thing. Delay in steps gives how many stages later a defect appears; delay in hours gives how long those stages took. Two practices touch these two units separately: integration frequency shortens the hours, pair programming shortens the steps. Because the delivery pipeline’s mechanics are run elsewhere in this curriculum, here the pipeline is nothing more than a period. The through-line is the regional library network, and it is fiction.
Where the Defect Is Found
The block below is a model, not a measurement.
PM25 — forty-eight work items are derived from a generator; the generator is self-written and the seed is visible. Every item has a size (3–12 hours) and a module it touches; the same set is used at every setting. Eight developers work.
PM26 — the earliest step a defect can be seen in is given as input, as a class. The path is five steps: writing, local, integration, release, operations. A writing-class defect is caught while it is being written if a second pair of eyes is present; otherwise it stays hidden until integration. An integration-class defect surfaces only once it is merged with someone else’s work, a release-class defect at release testing, and an operations-class defect only in operations. Release testing runs every forty hours, and the operations window opens every eighty hours.
PM27 — in a paired item, a writing-class defect is fixed on the spot and never becomes a separate work item. This is pair programming’s only gain in the model.
PM28 — every integration event costs a fixed 0.5 hours, plus 0.06 hours of overlap cost for every pair of items integrated in the same batch. This is where the price of infrequent integration comes from: as the batch grows, the pair count grows quadratically.
Rework is one-third of the item plus 1 hour for every item completed in the same module between the defect’s birth and the moment it is found.
// practice.mjs — feedback delay (model); integration frequency and pairing ratio are scanned import { writeFileSync } from "node:fs"; const DEVS = 8; // developer count const RELEASE = 40; // release testing runs on this cadence (hours) const OPERATIONS = 80; // operations window const FIXED = 0.5; // fixed cost of one integration event (hours) const OVERLAP = 0.06; // overlap cost for every pair of items integrated in the same batch (hours) const STEP = ["writing", "local", "integration", "release", "operations"]; // PM25: the generator is self-written, the seed is visible; the same item set is used at every setting. let seed = 20260127; const rand = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648; const MODULE = ["loan", "fee", "membership", "catalog", "branch", "identity"]; const ITEM = [...Array(48)].map((no) => { const size = 3 + Math.floor(rand() * 10); // 3-12 hours const m = MODULE[Math.floor(rand() * MODULE.length)]; const r = rand(); // PM26: the earliest step a defect can be seen in is a class, given as input const kind = r < 0.55 ? null : r < 0.76 ? "writing" : r < 0.90 ? "integration" : r < 0.96 ? "release" : "operations"; return { no, size, module: m, kind }; }); const next = (t, p) => Math.ceil((t + 1e-9) / p) * p; // the next periodic occurrence function run(T, ratio) { const free = Array(DEVS).fill(0); const item = ITEM.map((k, i) => ({ ...k, paired: i % 4 < Math.round(ratio * 4) })); let effort = 0; for (const k of item) { const count = k.paired ? 2 : 1; const chosen = free.map((t, i) => [t, i]).sort((a, b) => a[0] - b[0]).slice(0, count); const start = Math.max(...chosen.map(([t]) => t)); for (const [, i] of chosen) free[i] = start + k.size; k.end = start + k.size; effort += k.size * count; } // PM27: in a paired item, a writing-class defect is caught on the spot; others wait for their period const findings = []; for (const k of item) { if (!k.kind) continue; const [step, at] = k.kind === "writing" && k.paired ? [0, k.end] : k.kind === "release" ? [3, next(k.end, RELEASE)] : k.kind === "operations" ? [4, next(k.end, OPERATIONS)] : [2, next(k.end, T)]; const builtOn = item.filter((x) => x.module === k.module && x.end > k.end && x.end <= at).length; // a defect caught while writing is fixed on the spot; it never becomes a separate work item findings.push({ no: k.no, kind: k.kind, step, delayHour: at - k.end, rework: step === 0 ? 0 : Math.ceil(k.size / 3) + builtOn }); } const rework = findings.reduce((a, b) => a + b.rework, 0); // PM28: every integration event pays a fixed cost, plus overlap for every pair of items in the batch const last = Math.max(...item.map((k) => k.end)); let events = 0, overlapCost = 0; for (let t = T; t <= next(last, T); t += T) { const batch = item.filter((k) => k.end > t - T && k.end <= t).length; if (batch === 0) continue; events += 1; overlapCost += (OVERLAP * batch * (batch - 1)) / 2; } const integrationCost = +(events * FIXED + overlapCost).toFixed(1); const avg = (f) => findings.reduce((a, b) => a + f(b), 0) / findings.length; return { T, ratio, duration: last, findings, findingCount: findings.length, events, overlapCost: +overlapCost.toFixed(1), delayStep: avg((b) => b.step), delayHour: avg((b) => b.delayHour), pairFound: findings.filter((b) => b.step === 0).length, rework, integrationCost, effort, total: +(effort + rework + integrationCost).toFixed(1) }; } const strip = ({ findings, ...r }) => r; const FREQUENCY = [2, 4, 8, 16, 40].map((T) => run(T, 0)); const RATIO = [0, 0.25, 0.5, 0.75, 1].map((o) => run(4, o)); writeFileSync("practice.json", JSON.stringify({ FREQUENCY: FREQUENCY.map(strip), RATIO: RATIO.map(strip) })); console.log(`work items: ${ITEM.length}, base work: ${ITEM.reduce((a, k) => a + k.size, 0)} hours, ` + `developers: ${DEVS} (seed ${20260127})`); console.log(`defect class: ${["writing", "integration", "release", "operations"] .map((s) => `${s} ${ITEM.filter((k) => k.kind === s).length}`).join(", ")}; ` + `defect-free ${ITEM.filter((k) => !k.kind).length}`); console.log(`steps: ${STEP.map((a, i) => `${i} ${a}`).join(", ")}`); const base = FREQUENCY[1]; console.log(`\nintegration every 4 hours, no pairing: where the defect is found`); console.log(`${"defect class".padEnd(14)}${"count".padStart(6)}${"found at step".padStart(16)}` + `${"delay (steps)".padStart(16)}${"delay (hours)".padStart(16)}${"rework".padStart(12)}`); for (const s of ["writing", "integration", "release", "operations"]) { const g = base.findings.filter((b) => b.kind === s); const o = (f) => (g.reduce((a, b) => a + f(b), 0) / g.length).toFixed(1); console.log(`${s.padEnd(14)}${String(g.length).padStart(6)}${STEP[g[0].step].padStart(16)}` + `${o((b) => b.step).padStart(16)}${o((b) => b.delayHour).padStart(16)}` + `${String(g.reduce((a, b) => a + b.rework, 0)).padStart(12)}`); }
work items: 48, base work: 354 hours, developers: 8 (seed 20260127) defect class: writing 14, integration 5, release 2, operations 2; defect-free 25 steps: 0 writing, 1 local, 2 integration, 3 release, 4 operations integration every 4 hours, no pairing: where the defect is found defect class count found at step delay (steps) delay (hours) rework writing 14 integration 2.0 2.1 45 integration 5 integration 2.0 3.4 16 release 2 release 3.0 20.5 15 operations 2 operations 4.0 56.5 13
The table shows why feedback delay has to be measured in two units. The fourteen writing-class defects, with no pairing, are found two steps later, an average of 2.1 hours later; they wait for integration when they could have been fixed on the spot. At the other end there are only two operations defects, but their delay is 56.5 hours and they produce thirteen hours of rework — 6.5 hours per defect, twice the writing class. A small number of late-found defects is more expensive than a large number of early-found ones.
Scanning the Two Settings
// delay-scan.mjs — reads the results practice.mjs wrote; two settings are scanned separately import { readFileSync } from "node:fs"; const { FREQUENCY, RATIO } = JSON.parse(readFileSync("practice.json", "utf8")); const FIXED = 0.5; const table = (header, row, caption) => { const write = (h) => console.log(h.map((c, i) => String(c).padStart(header[i][1])).join("")); if (caption) { console.log(); console.log(caption); } write(header.map((b) => b[0])); row.forEach(write); }; table([["integration", 13], ["events", 8], ["delay (steps)", 16], ["delay (hours)", 16], ["rework", 12], ["integration cost", 19], ["total", 9]], FREQUENCY.map((r) => [`${r.T} h`, r.events, r.delayStep.toFixed(2), r.delayHour.toFixed(1), r.rework, r.integrationCost, r.total]), "integration frequency (no pairing):"); // PM29: the only cost of a shorter cadence is the fixed per-event cost; the break-even point follows. const a = FREQUENCY[0], b = FREQUENCY[1]; const breakEven = (b.rework + b.overlapCost - a.rework - a.overlapCost) / (a.events - b.events); console.log(`\nthe two-hour rhythm produces ${a.events} events, the four-hour rhythm ${b.events}; ` + `at a fixed cost of ${FIXED} hours per event, the two-hour rhythm comes out ${(b.total - a.total).toFixed(1)} hours ahead.`); console.log(`this lead disappears once the fixed cost per event passes ${breakEven.toFixed(2)} hours.`); table([["pairing ratio", 15], ["duration (hours)", 19], ["caught while writing", 23], ["delay (steps)", 16], ["delay (hours)", 16], ["rework", 12], ["effort", 9], ["total", 9]], RATIO.map((r) => [`${(100 * r.ratio).toFixed(0)}%`, r.duration, `${r.pairFound}/${r.findingCount}`, r.delayStep.toFixed(2), r.delayHour.toFixed(1), r.rework, r.effort, r.total]), "pairing ratio (integration every 4 hours):"); // PM30: if pairing's two-person cost exceeds its gain, at what multiplier does it break even? const y = RATIO[0], z = RATIO[RATIO.length - 1]; const multiplier = (z.effort - y.effort) / (y.rework - z.rework); console.log(`\nat 100% pairing, delay falls from ${y.delayHour.toFixed(1)} hours to ` + `${z.delayHour.toFixed(1)} hours, and rework from ${y.rework} hours to ${z.rework} hours; ` + `in exchange, effort rises from ${y.effort} hours to ${z.effort} hours and the work stretches ${z.duration - y.duration} hours.`); console.log(`rework per defect is ${(y.rework / y.findingCount).toFixed(1)} hours; ` + `for pairing to pay for itself, this number has to grow to ${multiplier.toFixed(1)} times that.`);
integration frequency (no pairing):
integration events delay (steps) delay (hours) rework integration cost total
2 h 21 2.26 8.0 82 13.3 449.3
4 h 13 2.26 8.7 89 12.3 455.3
8 h 7 2.26 10.0 94 13.6 461.6
16 h 4 2.26 13.4 103 22.9 479.9
40 h 2 2.26 21.4 132 53.6 539.6
the two-hour rhythm produces 21 events, the four-hour rhythm 13; at a fixed cost of 0.5 hours per event, the two-hour rhythm comes out 6.0 hours ahead.
this lead disappears once the fixed cost per event passes 1.25 hours.
pairing ratio (integration every 4 hours):
pairing ratio duration (hours) caught while writing delay (steps) delay (hours) rework effort total
0% 50 0/23 2.26 8.7 89 354 455.3
25% 62 4/23 1.91 7.8 69 443 523.8
50% 73 7/23 1.65 6.3 59 530 601.9
75% 85 9/23 1.48 5.3 50 615 677.5
100% 90 14/23 1.04 4.7 35 708 755.8
at 100% pairing, delay falls from 8.7 hours to 4.7 hours, and rework from 89 hours to 35 hours; in exchange, effort rises from 354 hours to 708 hours and the work stretches 40 hours.
rework per defect is 3.9 hours; for pairing to pay for itself, this number has to grow to 6.6 times that.
Two Practices, Two Separate Components
The first table’s most striking column is the one that does not move. Even as integration goes from every two hours to every forty, delay in steps stays fixed at 2.26: the defect is still found at the same step, only reaching that step takes longer. Delay in hours rises from 8.0 to 21.4, and rework from 82 to 132 hours. At the forty-hour rhythm, integration cost also jumps from 13.3 to 53.6 hours, because as the number of items integrated in a single batch grows, the overlap-pair count grows quadratically. Integrating more often loses nothing anywhere in this model; the two-hour rhythm is 6.0 hours ahead of the four-hour rhythm, and that lead only disappears once the fixed cost per event passes 1.25 hours. That is the number that needs to be measured: how long one integration event takes.
The second table does exactly the opposite. Pairing touches delay in steps — it falls from 2.26 to 1.04, because fourteen out of fourteen writing defects are caught at step zero and produce no rework at all. Rework falls from 89 to 35 hours. Its price, though, is on a scale seen nowhere else in the settings: effort rises from 354 hours to 708 hours, because the same work is now done by two people, and the work stretches by forty hours, because eight people now run half as many items at the same time.
The arithmetic is plain. 354 hours of extra effort buys 54 hours of rework savings. For pairing to pay for itself in this model, rework per defect has to grow from 3.9 hours to 6.6 times that. This multiplier is not imaginary: operations-class defects already run at 6.5 hours per defect, and if a defect slips into operations, rework climbs well above what this model shows. So pairing is an expensive habit where defects are cheap, and a cheap insurance policy where defects are expensive — and which one applies is a measurable question.
Summary
- Feedback delay was measured in two units: delay in steps gives how many stages later a defect appears, delay in hours gives how long those stages took.
- At four-hour integration, 14 writing defects are found 2 steps and 2.1 hours later; 2 operations defects are found 4 steps and 56.5 hours later and produce the most expensive rework at 6.5 hours per defect.
- Integration frequency never changes the step count (fixed at 2.26), but it changes the hours: going from 2 hours to 40 hours, delay rises from 8.0 to 21.4 hours, rework from 82 to 132 hours, integration cost from 13.3 to 53.6 hours.
- The only price of integrating more often is the fixed cost per event; the two-hour rhythm’s lead only disappears once that cost passes 1.25 hours.
- Pairing lowers delay in steps from 2.26 to 1.04 and rework from 89 to 35 hours, but raises effort from 354 to 708 hours and stretches the work by 40 hours; breaking even requires rework per defect to grow to 6.6 times its size.
Next Step
Five lessons chose the shape of the process, limited flow, counted the price of coordination, and shortened feedback delay. All five share one blind spot. Every one of these measurements took who hands off to whom as a given: the step boundary in the lifecycle model, the backlog’s owner in the iteration, the team the WIP limit covers in the flow model, which team a module falls to in the scaled arrangement. Every time, that boundary was an input, and it was never questioned. But that boundary comes from somewhere: teams were split some way, modules were divided up some way, and every boundary a work item crosses is really a team boundary. The next topic asks that question — how much does the way teams are split determine the architecture, and when the communication structure and the module structure do not overlap, how much do the waiting, rework, and coordination rounds counted across these five lessons grow.
To keep your progress and take notes, Log in
My notes
Log in to take notes.