Skip to content
academia.sh

Lesson 01 / 10

Lifecycle Models

Running waterfall, V, spiral, and incremental lifecycles on the same eighteen work items: handoffs, waiting at the gate, feedback delay, and rework are counted, and what each model loses where it wins is written down.

Contents

The previous course turned an architectural rule into an object: the rule was written, turned into a check, and what it caught and missed was counted. All of those measurements silently left one thing out. It is people who write the rule, run the check, disable the rule in the face of a false alarm, and make the next change; the order in which these people work, who they hand work to, and when they learn of a mistake is written directly into the architecture.

This course’s unit of measure is the work item: the path from the moment a change request is born to the moment it reaches operations. Every lesson counts three things — how many boundaries an item crosses and how many rounds it waits at each crossing, how many items come back and why, and how much these grow where the team boundary and the module boundary do not overlap. The through-line is the regional library network: branch systems, an externally purchased catalog, in-house loan and fee services, a separate membership system, and the municipality’s identity service. It is fiction.

The Work Item Set to Measure

The block below is a model, not a measurement: it is a data structure that makes it possible to run the same set of work items through four lifecycles.

PM1 — eighteen work items pass through five steps and a single team works them. Each item consumes 3 work units at every step, and the team processes 9 work units per round. The reasoning: models are only comparable under the same workload and the same capacity; what creates the difference must be batching, not the amount of work.

PM2 — ten of the items carry a defect that is born somewhere, and its birth step and type are known as input. The types are kept apart: missing information (a detail was never asked for), wrong boundary (two items hold the same data in different places, and the mistake shows up only once the second one is written), late-learned constraint (the constraint surfaces only in operations). Feedback delay can only be counted against a known set of defects.

PM3 — the batching rule is the model itself. Waterfall and V run a single batch; spiral splits into three cycles by risk order, and incremental splits into six increments by dependency order.

PM4 — the step a defect is found in depends on the model. Missing information and wrong boundary surface in testing; late-learned constraint surfaces in operations. In the V model, test design is done together with the implementation step, so missing information catches the eye in the step it is born in. Once a constraint is learned in operations, an item carrying the same constraint in a later batch is no longer born defective; the ones in the same batch have already been written.

PM5 — fixed costs depend on the model. In the V model, the first three steps each consume 1 extra work unit per item. Putting a batch into operation costs 6 work units, independent of the item count.

// lifecycle.mjs — runs the same 18 work items through four lifecycle models (model)
// defect code: e missing information, s wrong boundary, k late-learned constraint; digit is the step the defect is born in
import { writeFileSync } from "node:fs";

const STEP = ["analysis", "design", "implementation", "testing", "operations"];
const UNIT = 3;       // work units a work item consumes at one step
const CAPACITY = 9;   // work units per round (single team)
const RELEASE = 6;    // release overhead per batch, independent of item count

const ITEM = [
  "member verification front  |membership|2|e0",
  "catalog search front       |catalog   |1|-",
  "loan record creation       |loan      |2|s1 S1",
  "shelf status query         |catalog   |0|-",
  "late fee calculation       |fee       |2|k0 K1",
  "branch stock turnover      |branch    |1|e2",
  "membership suspension      |membership|1|s1 S1",
  "penalty notification       |fee       |0|-",
  "branch shelf label         |branch    |0|-",
  "reservation queue          |loan      |2|s1 S2",
  "identity matching          |membership|2|k0 K1",
  "catalog import             |catalog   |1|-",
  "fee refund                 |fee       |1|e2",
  "inter-branch transfer      |branch    |2|s1 S2",
  "loan extension             |loan      |0|-",
  "late report                |fee       |0|-",
  "member notification        |membership|0|k0 K2",
  "branch daily closeout      |branch    |1|-",
].map((line, i) => {
  const [name, module, risk, defect] = line.split("|").map((x) => x.trim());
  const [code, key = null] = defect.split(" ");
  const TYPE = { e: "missing information", s: "wrong boundary", k: "late constraint" };
  return { name, module, risk: +risk, increment: (i / 3) | 0, key,
           type: code === "-" ? null : TYPE[code[0]], origin: code === "-" ? -1 : +code[1] };
});

// PM3: the batching rule is the model itself; waterfall and V run a single batch, spiral splits
// into three cycles by risk order, incremental splits into six increments by dependency order.
const byRisk = [...ITEM].sort((a, b) => b.risk - a.risk);
const BATCH = {
  waterfall: [ITEM],
  V:         [ITEM],
  spiral:    [0, 1, 2].map((c) => byRisk.slice(c * 6, c * 6 + 6)),
  incremental: [0, 1, 2, 3, 4, 5].map((a) => ITEM.filter((k) => k.increment === a)),
};
const stepCost = (model, k, s) => (k.release ? RELEASE : UNIT + (model === "V" && s <= 2 ? 1 : 0));
// In V, test design is done together with the implementation step: missing information shows up in the step it is born in.
const foundStep = (model, k) =>
  (model === "V" && k.type === "missing information") ? k.origin : (k.type === "late constraint" ? 4 : 3);

function run(model) {
  const batches = BATCH[model];
  const state = new Map(ITEM.map((k) => [k.name, { stepFinish: new Map(), batch: 0 }]));
  batches.forEach((p, i) => p.forEach((k) => { state.get(k.name).batch = i; }));
  const learned = new Set(), resolved = new Set(), findings = [];
  let round = 0, waiting = 0, handoffs = 0, rework = 0, total = 0;

  const segment = (list, unitCost) => {                 // every segment starts a fresh round
    round += 1;
    let cap = CAPACITY;
    const finish = new Map();
    for (const k of list) {
      let remaining = unitCost(k);
      total += remaining;
      while (remaining > 0) {
        if (cap === 0) { round += 1; cap = CAPACITY; }
        const taken = Math.min(cap, remaining); cap -= taken; remaining -= taken;
      }
      finish.set(k.name, round);
    }
    return finish;
  };

  batches.forEach((batch, p) => {
    for (let s = 0; s < STEP.length; s++) {
      const list = s === 4 ? [...batch, { name: "__release", release: true }] : batch;
      const finish = segment(list, (k) => stepCost(model, k, s));
      for (const k of batch) {
        waiting += round - finish.get(k.name);
        state.get(k.name).stepFinish.set(s, finish.get(k.name));
      }
      handoffs += batch.length;

      const found = ITEM.filter((k) => {                // defects that surface at this step
        if (k.origin < 0) return false;
        if (k.type === "missing information") return batch.includes(k) && s === foundStep(model, k);
        if (k.type === "late constraint") return s === 4 && batch.includes(k) && !learned.has(k.key);
        const group = ITEM.filter((x) => x.key === k.key);   // wrong boundary: paired item
        return s === 3 && !resolved.has(k.key) &&
          group.every((x) => state.get(x.name).batch <= p) && group.some((x) => state.get(x.name).batch === p);
      });
      for (const k of found) {
        const steps = [...Array(s - k.origin + 1)].map((_, i) => k.origin + i);
        const cost = steps.reduce((a, x) => a + stepCost(model, k, x), 0);
        const foundRound = s === k.origin ? finish.get(k.name) : round;   // found in the same step: not gated
        findings.push({ model, item: k.name, type: k.type, origin: k.origin, found: s, cost,
          delayStep: (p - state.get(k.name).batch) * STEP.length + s - k.origin,
          delayRound: foundRound - state.get(k.name).stepFinish.get(k.origin),
          builtOn: ITEM.filter((x) => state.get(x.name).stepFinish.get(k.origin) <= foundRound).length });
        rework += cost;
        handoffs += steps.length;
        if (k.type === "wrong boundary") resolved.add(k.key);
      }
      if (found.length) segment(found, (k) =>
        [...Array(s - k.origin + 1)].reduce((a, _, i) => a + stepCost(model, k, k.origin + i), 0));
      if (s === 4) for (const k of batch) if (k.type === "late constraint") learned.add(k.key);
    }
  });
  return { model, round, waiting, handoffs, rework, total, findings };
}

const RESULT = ["waterfall", "V", "spiral", "incremental"].map(run);
writeFileSync("findings.json", JSON.stringify(RESULT.flatMap((s) => s.findings)));

const TYPE3 = ["missing information", "wrong boundary", "late constraint"];
const table = (headers, rows, caption) => {             // negative width left-aligns
  const write = (h) => console.log(h.map((c, i) =>
    headers[i][1] < 0 ? String(c).padEnd(-headers[i][1]) : String(c).padStart(headers[i][1])).join(""));
  console.log();
  if (caption) console.log(caption);
  write(headers.map((b) => b[0]));
  rows.forEach(write);
};
const avg = (g, f) => (g.length ? (g.reduce((a, b) => a + f(b), 0) / g.length) : null);

console.log(`work items: ${ITEM.length}, steps: ${STEP.length}, defective items: ` +
  `${ITEM.filter((k) => k.origin >= 0).length} ` +
  `(${TYPE3.map((t) => `${ITEM.filter((k) => k.type === t).length} ${t}`).join(", ")})`);

table([["model", -11], ["batch", 7], ["round", 7], ["waiting", 9], ["handoffs", 10], ["total units", 14]],
  RESULT.map((s) => [s.model, BATCH[s.model].length, s.round, s.waiting, s.handoffs, s.total]));

table([["model", -11], ["found defects", 15], ["avg. delay (steps)", 21], ["avg. delay (rounds)", 22],
       ["avg. built-on", 15], ["rework", 9]],
  RESULT.map((s) => [s.model, s.findings.length, avg(s.findings, (b) => b.delayStep).toFixed(2),
    avg(s.findings, (b) => b.delayRound).toFixed(2), avg(s.findings, (b) => b.builtOn).toFixed(2), s.rework]));

table([["model", -11], ["missing information", 22], ["wrong boundary", 17], ["late constraint", 18]],
  RESULT.map((s) => [s.model, ...TYPE3.map((t) => {
    const o = avg(s.findings.filter((b) => b.type === t), (b) => b.delayRound);
    return o === null ? "-" : o.toFixed(1);
  })]), "average delay by defect type (rounds):");
work items: 18, steps: 5, defective items: 10 (3 missing information, 4 wrong boundary, 3 late constraint)

model        batch  round  waiting  handoffs   total units
waterfall        1     43      243       125           381
V                1     50      288       120           440
spiral           3     47       63       125           393
incremental      6     48       18       120           396

model        found defects   avg. delay (steps)   avg. delay (rounds)  avg. built-on   rework
waterfall               10                 2.50                 20.10          18.00      105
V                       10                 2.00                 19.00          14.70      110
spiral                  10                 3.00                  8.50           9.60      105
incremental              9                 4.00                  5.22          10.67       90

average delay by defect type (rounds):
model         missing information   wrong boundary   late constraint
waterfall                    13.3             14.8              34.0
V                             0.0             18.5              38.7
spiral                        4.0              9.3              12.0
incremental                   1.7              7.8               5.5

Reading the Four Models

The first table measures flow, the second measures learning, and the two move in opposite directions.

Waterfall finishes in the fewest rounds (43) because it pays its fixed overhead once. Its price is in the second column: 243 rounds of waiting accumulate, because an item that finishes its own step still waits for the batch’s last item, and it waits again at every gate. The learning side is harsher — the delay is 20.1 rounds, and when a defect is found, eighteen of the eighteen items sit on top of that wrong assumption. For a late-learned constraint, the delay climbs to 34 rounds, because operations happens only once, at the very end.

The V model produces a clear gain on exactly one defect type: missing-information delay drops from 13.3 rounds to 0, because the question is asked at the step where the answer is needed. Its price shows up in two places. Total work effort rises from 381 to 440 units (270 base work, a 54-unit test-design surcharge, 6 units of release, 110 units of rework), and the flow stretches to 50 rounds. On the other two types, V does nothing at all — wrong boundary rises from 14.8 to 18.5 rounds and late-learned constraint from 34 to 38.7, only because the whole run is longer.

Spiral splits the batch by risk order; because the six riskiest items go all the way to operations in the first cycle, late-learned constraint delay drops from 34 rounds to 12, average delay to 8.5 rounds, and waiting to 63. Its price is the release overhead paid three times (18 units) and one missed learning opportunity: because the risk order puts the two items carrying the same constraint (late fee calculation and identity matching) in the same cycle, the constraint is learned only after both have already been written, and both go into rework.

The incremental model splits into six increments by dependency order. Waiting drops to 18 rounds, delay to 5.22 rounds, and found defects drop from ten to nine: the constraint learned in the first increment means the twin item in the third increment is born without the defect, and rework falls to 90 units. Its price is in rounds — six release overheads stretch the flow to 48 rounds, the longest of the four models — and its step-based delay is the highest of all (4.00): the wrong boundary that crosses an increment boundary shows up only in the second increment, reopening an item the first increment had already delivered.

How Delay Turns Into Rework

PM6 — rework’s cost comes from two factors: the number of steps redone and the number of items built on top of the same assumption. The block below pools the thirty-nine findings from the four runs and breaks these two factors out against delay.

// delay.mjs — reads the findings lifecycle.mjs wrote; delay against rework across all four runs
import { readFileSync } from "node:fs";

const FINDINGS = JSON.parse(readFileSync("findings.json", "utf8"));
const BUCKET = [[0, 0], [1, 5], [6, 12], [13, 24], [25, 99]];

console.log(`${FINDINGS.length} total defect findings across four models; bucket = feedback delay (rounds)\n`);
console.log(`${"delay".padStart(9)}${"found".padStart(7)}${"avg. rework (units)".padStart(21)}` +
  `${"avg. steps redone".padStart(19)}${"avg. built-on items".padStart(21)}`);
for (const [a, b] of BUCKET) {
  const g = FINDINGS.filter((x) => x.delayRound >= a && x.delayRound <= b);
  if (!g.length) continue;
  const avg = (f) => (g.reduce((s, x) => s + f(x), 0) / g.length).toFixed(1);
  console.log(`${(a === b ? `${a}` : `${a}-${b}`).padStart(9)}${String(g.length).padStart(7)}` +
    `${avg((x) => x.cost).padStart(21)}${avg((x) => x.found - x.origin + 1).padStart(19)}` +
    `${avg((x) => x.builtOn).padStart(21)}`);
}

// PM6: rework's cost comes from two factors; the number of steps redone and the number of items
// built on the same assumption. Both grow along with the delay.
const STEP_NAME = (i) => ["analysis", "design", "implementation", "testing", "operations"][i];
const joinUnique = (g, f) => [...new Set(g.map(f))].join(" and ");
const costliest = [...FINDINGS].sort((a, b) => b.cost - a.cost).slice(0, 5);
console.log(`\ntop five costliest findings: type ${joinUnique(costliest, (x) => x.type)}, ` +
  `origin ${joinUnique(costliest, (x) => STEP_NAME(x.origin))}, found ${joinUnique(costliest, (x) => STEP_NAME(x.found))}, ` +
  `avg. ${(costliest.reduce((a, x) => a + x.cost, 0) / 5).toFixed(1)} units of rework`);
39 total defect findings across four models; bucket = feedback delay (rounds)

    delay  found  avg. rework (units)  avg. steps redone  avg. built-on items
        0      3                  4.0                1.0                  7.0
      1-5     11                  8.7                2.9                 10.4
     6-12      6                 10.5                3.5                 13.5
    13-24     13                 10.8                3.4                 15.0
    25-99      6                 16.5                5.0                 18.0

top five costliest findings: type late constraint, origin analysis, found operations, avg. 16.8 units of rework

The buckets move in a single direction: rework is 4 units when delay is zero, and 16.5 units once delay passes twenty-five rounds. The gap comes from two factors. Steps redone rises from 1.0 to 5.0, because a defect found late invalidates more steps; items built on the same assumption rises from 7.0 to 18.0, because work has piled up around the defect in the meantime. All five of the costliest findings are of the same type — a late-learned constraint born in analysis and found in operations.

Base work is 270 units in all four models; the difference is created by fixed overhead and rework. There is no single number that says one model is “better”: small batches win where late-learned constraints dominate, early validation wins where missing information dominates, and large batches win where the fixed release cost is high.

Summary

  • The same 18 work items were run through four lifecycles; the 270 units of base work were held fixed, so only the batching rule and the fixed overhead created the difference.
  • Waterfall gives the shortest flow (43 rounds) but accumulates 243 rounds of waiting, pushes delay to 20.1 rounds, and leaves all 18 items sitting on the wrong assumption at every defect.
  • The V model cuts missing-information delay from 13.3 rounds to 0; in exchange, total work effort rises from 381 to 440 units and delay rises for the other two defect types.
  • Spiral cuts delay to 8.5 rounds but puts two items carrying the same constraint into the same cycle and misses the cross-batch learning; incremental cuts waiting to 18 rounds and rework to 90 units and never even births one defect, at the price of the longest flow, 48 rounds.
  • Across the pooled 39 findings, once delay passes 25 rounds rework climbs from 4 to 16.5 units; steps redone rises from 1.0 to 5.0, and items built on top rise from 7.0 to 18.0.

Next Step

This lesson’s single strongest variable was batch size: the difference between one batch of eighteen items and six batches of three items changed waiting by a factor of thirteen and feedback delay by a factor of four. But batch size was chosen by hand here — nowhere was it measured why six increments and not some other number. The next lesson takes up iterative delivery frameworks by their common structure and scans the real variable: how flow time, rework, and time spent in ceremony change as iteration length grows from one week to four weeks. A short iteration lowers delay, but every iteration’s planning and review overhead is fixed, and an item that does not fit in the iteration is left half-done and carried over to the next one. Because the two costs grow in opposite directions, the scan does not produce a single best length.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close