Lesson 11 / 11
Root Cause Analysis
Tracing a recurring defect class to its source: building the causal chain as a data structure, comparing the intervention that fixes the symptom with the one that fixes the cause on the same defect set, coverage growing while applicability drops with depth in the chain, and the review threshold's source.
Contents
Metrics count a class coming up again and again but do not say why. If eight schema mismatches were fixed one by one, whether there is a single shared cause under those eight shows up in no metric. A class recurring is not a defect, it is a symptom.
Root cause analysis is the method that goes from symptom to cause. Its tool is the causal chain: a sequence of links that starts from a defect and moves forward by asking “what caused this” at every step — known by the common name five whys. The chain is stopped somewhere, and an action is written there. The action that closes the symptom is called corrective action, the action that cuts the class’s recurrence is called preventive action. This lesson’s question is where to stop in the chain; the resource distributed is review hours.
The Chain Is a Data Structure
The chain’s first two links are class-specific: why this defect occurred, why that cause existed. From the third on, links are shared — several classes feed from the same process gap. Node count drops with depth, class-per-node rises. In exchange, a deep link falls outside the team’s control: what needs to change is not in the code, it is in the organization.
TP24 (assumption) — reviewing one link takes 25 minutes, the review budget across the horizon is 400 minutes, and a review lands with a delay of one release per depth. The budget’s source is the 120 minutes of per-period attention in the Quality Metrics lesson. TP25 (assumption) — preventive action’s cost at link k is 60, 240, 720, and 2,400 minutes; a deeper link concerns more parties. TP26 (assumption) — the action’s applicability share drops with depth: 0.90 / 0.70 / 0.40 / 0.00. The fifth link is outside the team. TP27 (assumption) — the root cause threshold is 3 recurrences. The threshold’s source is a measurement: the per-class recurrence average is 60/14 = 4.29, and threshold 3 also covers one step below that.
// library/defects.mjs -- the first lesson's defect set, exactly (seed 20260731, 60 defects, // 14 classes). This lesson does not use the inventory's cost columns, only the defect set. const WEIGHT = [["rule-boundary", 9], ["schema-mismatch", 6], ["migration-data-loss", 4], ["contract-field", 5], ["interface-state", 8], ["visual-deviation", 3], ["data-growth", 4], ["sql-concatenation", 3], ["known-vulnerability", 3], ["accessibility", 4], ["degraded-response", 3], ["recovery-gap", 2], ["off-scenario", 5], ["semantic-drift", 4]]; export function defects(seed, n) { let x = seed; const random = () => { x = (1103515245 * x + 12345) % 2147483648; return x / 2147483648; }; const total = WEIGHT.reduce((a, [, w]) => a + w, 0); const list = []; for (let i = 1; i <= n; i += 1) { let p = random() * total, defectClass = WEIGHT[WEIGHT.length - 1][0]; for (const [s, w] of WEIGHT) { p -= w; if (p < 0) { defectClass = s; break; } } list.push({ no: i, defectClass, likelihood: 1 + Math.floor(random() * 5), impact: 1 + Math.floor(random() * 5), fix: 15 + 15 * Math.floor(random() * 8) }); } return list; }
Five Interventions, One Defect Set
The model runs twenty releases. When a class recurs as many times as the threshold, a review opens for its k-th link; once it lands, every class that node feeds has its recurrence cut by the applicability share. I1 opens no review at all.
// library/chain.mjs -- builds the causal chain as a data structure and compares the // intervention that fixes the symptom with the one that fixes the cause, on the same defect set. Input: library/defects.mjs import { defects } from "./defects.mjs"; const SEED = 20260731, N = 60, RELEASES = 20, PER_RELEASE = N / RELEASES, MULTIPLIER = 6; // TP5, TP6 const REVIEW = 25, BUDGET = 400; // TP24: review min per link, horizon's review budget const COST = [0, 0, 60, 240, 720, 2400]; // TP25: preventive action cost at link k const APPLICABILITY = [0, 0, 0.9, 0.7, 0.4, 0]; // TP26: the action's applicability share const v = (x, n = 2) => x.toFixed(n); const g = (x, n) => String(x).padStart(n); // Causal chain: the first two links are class-specific, the third onward is shared across // classes. Nodes shrink with depth, because the same cause feeds more classes. const L3 = {}, L4 = {}; for (const [cause, sub] of [ ["schema-change-not-reviewed", ["schema-mismatch", "migration-data-loss", "contract-field"]], ["boundary-value-not-documented", ["rule-boundary", "interface-state"]], ["baseline-image-not-updated", ["visual-deviation"]], ["scale-assumption-not-documented", ["data-growth", "degraded-response", "recovery-gap"]], ["security-rule-missing-from-review", ["sql-concatenation", "known-vulnerability", "accessibility"]], ["acceptance-criterion-missing", ["off-scenario", "semantic-drift"]]]) for (const s of sub) L3[s] = cause; for (const [cause, sub] of [ ["no-check-by-change-type", ["schema-change-not-reviewed", "boundary-value-not-documented", "baseline-image-not-updated"]], ["assumption-not-documented", ["scale-assumption-not-documented", "security-rule-missing-from-review", "acceptance-criterion-missing"]]]) for (const s of sub) L4[s] = cause; const linkOf = (s, k) => (k <= 2 ? `${s}#${k}` : k === 3 ? L3[s] : k === 4 ? L4[L3[s]] : "process-owner-not-assigned"); // The five classes the second lesson's nine budget-fitting items miss escape to production. const ESCAPES = new Set(["accessibility", "degraded-response", "recovery-gap", "off-scenario", "semantic-drift"]); const DEFECTS = defects(SEED, N); const CLASSES = [...new Set(DEFECTS.map((k) => k.defectClass))]; // Horizon: RELEASES releases, PER_RELEASE defects per release. When a class recurs threshold // times, a review opens for its k-th link; a review takes one release per depth (TP24), and // once it lands, every class the node feeds has its recurrence cut by APPLICABILITY[k]. function horizon(depth, threshold) { const counter = new Map(), prevented = new Map(), open = new Set(), pending = []; let recur = 0, corrective = 0, preventive = 0, review = 0, missed = 0, when = 0, late = 0; for (let s = 1; s <= RELEASES; s += 1) { for (const item of pending) if (item.s === s) prevented.set(item.link, item.share); for (const k of DEFECTS.slice((s - 1) * PER_RELEASE, s * PER_RELEASE)) { const link = depth >= 2 ? linkOf(k.defectClass, depth) : null; const occurrence = 1 - (link ? prevented.get(link) || 0 : 0); if (occurrence <= 0) continue; // A review that has opened but has not yet landed still counts recurrence as delayed. if (link && open.has(link) && prevented.has(link) === false) late += occurrence; recur += occurrence; corrective += occurrence * k.fix; if (ESCAPES.has(k.defectClass)) missed += occurrence; const c = (counter.get(k.defectClass) || 0) + occurrence; counter.set(k.defectClass, c); if (depth >= 2 && c >= threshold && open.has(link) === false) { open.add(link); review += REVIEW * depth; preventive += COST[depth]; pending.push({ link, s: s + depth - 1, share: APPLICABILITY[depth] }); when += s + depth - 1; } } } const total = corrective + review + preventive + MULTIPLIER * missed * 60; // TP5: escaped defect's multiplier return { opened: open.size, review, preventive, corrective, recur, missed, late, when: open.size ? when / open.size : 0, total, fits: review <= BUDGET }; } console.log(`defect set seed ${SEED}, ${N} defects, ${CLASSES.length} classes, ${RELEASES} releases`); const counts = CLASSES.map((s) => DEFECTS.filter((k) => k.defectClass === s).length); console.log(`recurrence per class: minimum ${Math.min(...counts)}, maximum ${Math.max(...counts)}, average ${v(N / CLASSES.length)}`); console.log("\nchain structure -- nodes shrink with depth, applicability drops"); console.log(" link node class per node applicability prevent min expected prevented"); for (const k of [1, 2, 3, 4, 5]) { const d = new Set(CLASSES.map((s) => linkOf(s, k))); const coverage = CLASSES.length / d.size; console.log(g(k, 5) + g(d.size, 7) + g(v(coverage), 20) + g(k >= 2 ? v(APPLICABILITY[k]) : "-", 18) + g(k >= 2 ? COST[k] : "-", 13) + g(k >= 2 ? v(coverage * APPLICABILITY[k]) : "-", 24)); } const NAMES = ["I1 fix the symptom", "I2 stop at link 2", "I3 stop at link 3", "I4 stop at link 4", "I5 stop at link 5"]; console.log(`\nfive interventions on the same defect set (threshold TP27 = 3, review budget TP24 = ${BUDGET} min)`); console.log("intervention opened review budget preventive corrective recur delayed missed in effect total min"); for (let k = 1; k <= 5; k += 1) { const o = horizon(k, 3); console.log(NAMES[k - 1].padEnd(24) + g(o.opened, 8) + g(o.review, 10) + g(o.fits ? "fits" : "over", 8) + g(o.preventive, 12) + g(v(o.corrective, 0), 12) + g(v(o.recur), 10) + g(v(o.late), 11) + g(v(o.missed), 8) + g(o.opened ? v(o.when, 1) : "-", 11) + g(v(o.total, 0), 11)); } console.log("\nthreshold TP27's sensitivity -- stopping at link 3"); console.log(" threshold opened review min budget recur missed total min"); for (const e of [2, 3, 5]) { const o = horizon(3, e); console.log(g(e, 11) + g(o.opened, 8) + g(o.review, 13) + g(o.fits ? "fits" : "over", 8) + g(v(o.recur), 10) + g(v(o.missed), 8) + g(v(o.total, 0), 11)); }
defect set seed 20260731, 60 defects, 14 classes, 20 releases
recurrence per class: minimum 1, maximum 7, average 4.29
chain structure -- nodes shrink with depth, applicability drops
link node class per node applicability prevent min expected prevented
1 14 1.00 - - -
2 14 1.00 0.90 60 0.90
3 6 2.33 0.70 240 1.63
4 2 7.00 0.40 720 2.80
5 1 14.00 0.00 2400 0.00
five interventions on the same defect set (threshold TP27 = 3, review budget TP24 = 400 min)
intervention opened review budget preventive corrective recur delayed missed in effect total min
I1 fix the symptom 0 0 fits 0 4125 60.00 0.00 18.00 - 10605
I2 stop at link 2 12 600 over 720 2870 42.00 1.00 14.40 12.5 9374
I3 stop at link 3 5 375 fits 1200 2813 40.40 8.00 15.20 10.8 9860
I4 stop at link 4 2 200 fits 1440 3075 44.40 7.00 15.60 8.0 10331
I5 stop at link 5 1 125 fits 2400 4125 60.00 11.00 18.00 9.0 13130
threshold TP27's sensitivity -- stopping at link 3
threshold opened review min budget recur missed total min
2 6 450 over 29.90 10.30 7707
3 5 375 fits 40.40 15.20 9860
5 4 300 fits 53.00 17.30 11204
Symptom and Cause
I1 fixes sixty defects one by one: 4,125 minutes of corrective action, sixty recurrences, eighteen escaped defects. No class stops, because every intervention only closes that one defect. I3 opens five reviews, drops recurrence to 40.40, escaped defects to 15.20, and pulls corrective cost down to 2,813 minutes; the 1,312 minutes in between are fixes never written.
This does not mean preventive action is free. I3 pays 1,200 minutes for preventive action, 375 for review; its total is 9,860 minutes, 745 minutes better than I1’s 10,605. Cutting recurrence pays off in the share of the recurrence it cuts that would have escaped.
I2 gives the lowest total (9,374 minutes), but it opens twelve reviews and asks for 600 minutes; TP24’s budget is 400. The best row on paper does not fit the budget; the cheapest intervention that fits is I3.
Depth’s Two Sides
The structure table separates depth’s two opposing effects. Class per node rises from 1.00 to 14.00 — a deeper cause feeds more classes — but applicability drops from 0.90 to 0.00. Their product is 0.90, 1.63, 2.80, and 0.00: highest at the fourth link, zero at the fifth.
Still, this ranking does not decide alone. I4 is the link with the highest expected coverage, but it does worse than I3 over the horizon (44.40 recurrences against 40.40). The reason is delay: depth k delays the action’s landing by k−1 releases, and the defect recurring while delayed is 1.00 for I2, 8.00 for I3, 11.00 for I5. Going deeper is not just expensive, it is slow; slowness produces its own recurrence.
The Fifth Link’s Cost
I5 is this lesson’s clearest number. It opens a single review, pays 125 minutes of review and 2,400 minutes of preventive action, and then nothing changes: recurrence 60.00, escaped defects 18.00 — both identical to I1’s. Its total is 13,130 minutes, 2,525 minutes more expensive than reviewing nothing at all.
The chain is correct; the problem is not in the chain. “Process owner not assigned” genuinely feeds all fourteen classes. But the action written at that link is not one the team can carry out, and an action not carried out prevents zero defects. Five whys is not a race to depth: the place to stop is the last link the team can actually change.
Threshold and the Return on the Decision
The threshold scan repeats the same pattern. Threshold 2 gives the lowest total (7,707 minutes, 29.90 recurrences, 10.30 escaped), but it opens six reviews and asks for 450 minutes — it exceeds the budget. Threshold 5 fits but leaves recurrence at 53.00. The threshold’s source is not a requirement but a two-sided measurement: the per-class recurrence average gives the floor, the review budget gives the ceiling.
The decision’s return is two queues. Corrective action closes a record and ends with the defect’s owner. Preventive action opens a process change — a review step, a checklist item, a documented assumption — and its closing depends on an approval outside the team. A root cause record opened without an owner written down meets the same end as the planning topic’s ownerless items: covered on paper, not run in practice.
Summary
- The causal chain is a data structure: the first two links are class-specific, the rest are shared, and node count drops from 14 to 1.
- Fixing the symptom leaves sixty recurrences and eighteen escaped defects unchanged; stopping at link 3 drops recurrence to 40.40, escaped defects to 15.20.
- Depth has two sides: class per node rises from 1.00 to 14.00 while applicability drops from 0.90 to 0.00. Depth is also delay — the defect recurring while delayed is 1.00 for I2, 8.00 for I3, 11.00 for I5.
- Stopping at the fifth link pays 2,525 minutes for nothing: recurrence and escaped defects stay identical to not reviewing at all.
- The threshold’s source is a two-sided measurement: the per-class recurrence average sets the floor, the review budget sets the ceiling.
Course Wrap-Up
This course’s eleven lessons followed the same pattern: a limited resource was distributed, and the escaped defects and feedback time the distribution produced were measured.
| Lesson | Resource distributed | Distribution decision | Escaped defects / feedback time |
|---|---|---|---|
| Test Plan | 120 min test budget per release | nine tests fitting the budget | 18 escaped / 16.5 min (full coverage 10 / 40.8) |
| Risk-Based Prioritization | same 120 min, order free | estimate/minute order | 18 escaped, escaped risk 155 / 18.2 min |
| Exploratory Testing | 120 min session = width × depth | four paths, depth five | 10.39 escaped / 74.8 min |
| Acceptance Criteria | 300 min product-side attention | seventeen semantic criteria (204 min) | 0 escaped / acceptance review stage |
| User Acceptance Testing | 600 user-minutes, twelve tasks | by usage share | 10 escaped / 26.0 min to finding |
| Testing in Continuous Integration | 1021 model min run | three stages and fail fast | 5 escaped / 228.1 min |
| Management of Test Environments | environment count and environment-minutes | production-like environment to nightly stage | 5 escaped / 252.6 min |
| Secrets and Data Management | secrets entering the test environment | narrowing scope from 511,800 to 1,809 records | 5 escaped, unchanged / 263.5 min |
| Reporting | 15 min attention, steps opened | grouped report | 0 roots escaped / 255.7 min |
| Quality Metrics | 120 min attention per period | two decision-changing metrics | leakage rate 6.25% → 12.50% / 386.8 → 398.0 min |
| Root Cause Analysis | 400 min review across the horizon | stopping at the third link | 15.20 escaped / lands at release 10.8 |
There is no zero in the last column. This is the course’s rule: every distribution produces escaped defects. There is no undivided state for dividing a limited resource; the plan that fits the budget misses eighteen defects, and the plan that does not fit does not run at all. A plan that wants zero escaped defects consumes the budget — in the Exploratory Testing lesson, doubling the session budget dropped escaped defects to 6.27 while the net benefit went negative, and that is this rule counted out. The only way to defend a distribution is to write out its escaped defects and its feedback time together.
This course was the fifth and last of the Software Quality and Testing curriculum. Quality and Testing Fundamentals built the concepts: defect, class, level, and oracle. Unit Testing and Test-Driven Development worked the cheapest link — the fastest-running, narrowest-seeing test. Integration, Contract and End-to-End Testing ran real boundaries and asked whether the fake matched the real thing. Non-Functional Testing tested behavior under load, attack, and failure with thresholds. This course placed all of them within time and attention: which test runs when, what decision a red result produces. The shared conclusion fits in one sentence — a test suite is only meaningful within a budget, and the decision that divides that budget is made in the reader’s own team. The numbers here show that decision’s shape; they do not take its place.
To keep your progress and take notes, Log in
My notes
Log in to take notes.