Skip to content
academia.sh

Lesson 02 / 12

Throwing Over the Wall

The delay and risk produced by separate teams: of the 29 context items carried by the same 18 changes, 21 are not written down; in the separate arrangement, 14 items drop at the boundary, producing 10 boundary bounces and 4 production errors; under shared responsibility the dropped items fall to 5, bounces to 3, production errors to 2, and the change failure rate falls from 22% to 11% — but deployment frequency falls from 9.5 to 7.9, wait at the internal transition rises from 8.1 to 12.2 steps, and time to restore rises from 61.0 to 65.0 steps; for the 7 changes carrying silent context, both arrangements produce the same 3 bounces and 2 errors.

Contents

The previous lesson measured the split in observation scope: four of the twenty-two occurrences never reached their owner, and sharing three surfaces brought the reach time down from 7.0 to 5.0 steps. Observation is not the only thing that is split. A change’s path from idea to production is split too, and the change is thrown over that split.

Throwing over the wall is handing a change across a team boundary before the context it carries has been written down. What gets thrown is not the change itself but what is known about it: which setting changed, which data format is expected, how to roll it back. This lesson runs the same set of changes through two arrangements and counts the context that drops at the boundary. The Process, Team and Delivery course counted handoff along a work item’s path across teams; the scale here is different — what is counted is the team boundary on a change’s path from idea to production, and the context that drops at that boundary.

The Change’s Path and the Context It Carries

The example is again the fictional regional measurement network; the network and the teams are both fictional. CF7: a change’s path is seven steps — definition, writing, review, test setup, acceptance test, production setup, production verification; in the separate arrangement the last four steps sit with the operations team. CF8: eighteen changes enter the flow eight steps apart, and each team runs a single change at a time. CF9: every change carries context items; where an item sits depends on its class — setting, data format, dependency, and rollback sit with the development team, scheduling and resource sit with the operations team. CF10: an item is in one of three states: written (in hand for both roles), askable (not written but can be asked of the other side), silent (not written and no one knows it is missing). CF11: if the role running the step is not the role the item sits with and the item is not written, then under shared responsibility an askable item passes at a cost of 2 steps of asking, and a silent item drops; in the separate arrangement, both drop. A dropped item is learned in that same run. CF12: if the dropped item is before production verification, the change returns to review and writes 3 steps of rework; if it is at production verification, the change becomes a production error, takes 4 steps, and the fix path runs from the start. A team boundary crossing writes 5 steps of wait, an internal one writes 1.

Under shared responsibility, test setup and production verification pass to the development team. The path and the two arrangements are a process-internal model; there is no real pipeline or change record.

// wall/flow.mjs — a change's path from idea to production (model): there is no real
// pipeline or change record, steps are a data structure. No randomness.

export const BOUNDARY = 5, INTERNAL = 1, ASK = 2, REWORK = 3, REPAIR = 4, ARRIVAL = 8, LIMIT = 900;
export const REVIEW_STEP = 2;                 // a change bouncing at the boundary returns to review
export const ROLE = ["dev", "ops"];

export const STEP = [["definition", "dev", 2], ["writing", "dev", 0], ["review", "dev", 2],
  ["test-setup", "ops", 2], ["acceptance-test", "ops", 3],
  ["production-setup", "ops", 2], ["production-verification", "ops", 2]]
  .map(([name, role, duration]) => ({ name, role, duration }));
export const SHARED = new Set(["test-setup", "production-verification"]);
const SHORT = { k: "test-setup", s: "acceptance-test", u: "production-setup",
  d: "production-verification" };
// source of a context item's class: setting/data-format/dependency/rollback sit with dev,
// scheduling and resource sit with ops. Item state: w written, a not written but askable,
// s not written and silent (no one knows it is missing).
export const OWNER = { setting: "dev", "data-format": "dev", dependency: "dev",
  rollback: "dev", scheduling: "ops", resource: "ops" };

// change: name, writing step size, context items it carries (class/step it is needed at/state)
export const CHANGE = `
  c01  5  setting/k/a         data-format/s/w
  c02  3  scheduling/u/a
  c03  6  dependency/k/w      resource/u/a
  c04  4  setting/s/a         rollback/d/a
  c05  7  data-format/k/s
  c06  3  scheduling/d/s      setting/u/w
  c07  5  resource/s/w        dependency/u/a
  c08  4  rollback/d/a
  c09  6  setting/k/a         scheduling/s/a
  c10  3  data-format/u/a
  c11  5  dependency/d/s      resource/k/w
  c12  4  setting/s/w
  c13  6  scheduling/k/s      rollback/u/a
  c14  3  resource/d/s
  c15  5  data-format/s/a     dependency/k/w
  c16  4  setting/u/s         scheduling/d/w
  c17  6  rollback/s/a
  c18  3  resource/k/s        data-format/d/a`
  .trim().split("\n").map((r) => r.trim().split(/\s+/)).map(([name, size, ...k]) => ({
    name, size: +size, item: k.map((p) => { const [cls, a, s] = p.split("/");
      return { cls, step: SHORT[a], state: s, owner: OWNER[cls] }; }) }));

export function run(arrangement) {
  const step = STEP.map((a) => ({ ...a,
    role: arrangement === "shared" && SHARED.has(a.name) ? "dev" : a.role }));
  const D = CHANGE.map((d, i) => ({ ...d, i, step: 0, ready: i * ARRIVAL, extra: 0, transition: null,
    known: new Set(d.item.filter((k) => k.state === "w").map((k) => k.cls)),
    lost: null, dropped: [], done: false, prodDone: null, errorAt: null,
    recovery: null, error: false, bounced: 0 }));
  const server = { dev: null, ops: null };
  const S = { lost: 0, bounced: 0, prodError: 0, deploy: 0, ask: 0, rework: 0,
    boundaryCross: 0, boundaryWait: 0, internalCross: 0, internalWait: 0 };

  const advance = (d, target, t, extra) => {
    const isBoundary = step[target].role !== step[d.step].role;
    if (isBoundary) { S.boundaryCross++; S.boundaryWait += BOUNDARY; d.transition = "boundary"; }
    else { S.internalCross++; S.internalWait += INTERNAL; d.transition = "internal"; }
    d.ready = t + (isBoundary ? BOUNDARY : INTERNAL); d.step = target; d.extra = extra;
  };

  const start = (d, r) => {
    const a = step[d.step];
    let duration = (a.name === "writing" ? d.size : a.duration) + d.extra;
    d.extra = 0; d.lost = null;
    for (const k of d.item.filter((k) => k.step === a.name && !d.known.has(k.cls))) {
      d.known.add(k.cls);                     // the item is learned here regardless of state
      if (k.owner === r) continue;             // the role running this step already carries the item
      if (arrangement === "shared" && k.state === "a") {   // it is asked of the other side, cost is a step
        duration += ASK; S.ask += ASK; continue;
      }
      S.lost++; d.lost = k; d.dropped.push(k);          // dropped at the boundary
    }
    d.remaining = duration;
  };

  const finish = (d, t) => {
    const a = step[d.step];
    if (a.name === "production-setup") { S.deploy++; if (d.prodDone === null) d.prodDone = t; }
    if (d.lost) {
      const inProd = a.name === "production-verification";
      if (inProd) { S.prodError++; d.error = true; d.errorAt = t; } else { S.bounced++; d.bounced++; }
      S.rework += inProd ? REPAIR : REWORK;
      return advance(d, REVIEW_STEP, t, inProd ? REPAIR : REWORK);
    }
    if (d.step < step.length - 1) return advance(d, d.step + 1, t, 0);
    if (d.error && d.recovery === null) d.recovery = t - d.errorAt;
    d.done = true;
  };

  for (let t = 0; t < LIMIT; t++) for (const r of ROLE) {
    if (server[r] === null) {
      const candidate = D.filter((d) => !d.done && d !== server.dev && d !== server.ops
        && d.ready <= t && step[d.step].role === r).sort((a, b) => a.ready - b.ready || a.i - b.i)[0];
      if (candidate) {
        if (candidate.transition === "boundary") S.boundaryWait += t - candidate.ready;
        else if (candidate.transition === "internal") S.internalWait += t - candidate.ready;
        start(candidate, r); server[r] = candidate;
      }
    }
    const d = server[r];
    if (d && --d.remaining === 0) { server[r] = null; finish(d, t + 1); }
  }
  return { D, S, window: Math.max(...D.map((d) => d.prodDone ?? 0)) };
}

export const avg = (a) => (a.length ? (a.reduce((s, v) => s + v, 0) / a.length).toFixed(1) : "-");

export function measure(arrangement) {
  const { D, S, window } = run(arrangement);
  const lead = D.map((d) => d.prodDone - d.i * ARRIVAL);
  return { D, S, window,
    boundaryCross: S.boundaryCross, waitPerBoundary: (S.boundaryWait / S.boundaryCross).toFixed(1),
    waitPerInternal: (S.internalWait / S.internalCross).toFixed(1), infoLoss: S.lost,
    ask: S.ask, bounced: S.bounced, prodErrors: S.prodError, rework: S.rework,
    leadTime: avg(lead), deployFrequency: (100 * S.deploy / window).toFixed(1),
    changeFailureRate: (100 * D.filter((d) => d.error).length / D.length).toFixed(0) + "%",
    recovery: avg(D.filter((d) => d.error).map((d) => d.recovery)),
    unfinished: D.filter((d) => !d.done).length };
}

The four delivery metrics are read from the same run: deployment frequency is production setups completed per 100 steps (including redeployments), lead time is the steps from a change entering the flow to its first production setup, change failure rate is the share of changes that produce an error in production, time to restore is the steps from an error surfacing to the fix being verified in production.

// wall/measure.mjs — the same 18 changes in two arrangements: boundary wait, information loss, production errors.
import { CHANGE, STEP, SHARED, ARRIVAL, measure, avg } from "./flow.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const item = CHANGE.flatMap((d) => d.item);
const count = (f) => item.filter(f).length;
const R = { separate: measure("separate"), shared: measure("shared") };

console.log(`${CHANGE.length} changes, ${STEP.length} steps, ${item.length} context items: ` +
  `${count((k) => k.state === "w")} written, ${count((k) => k.state === "a")} askable, ` +
  `${count((k) => k.state === "s")} silent`);
console.log(`  where items sit: ` + ["dev", "ops"].map((r) =>
  `${r} ${count((k) => k.owner === r)}`).join(", "));
console.log(`  separate: ${STEP.filter((a) => a.role === "ops").length} steps sit with ops; ` +
  `shared: ${[...SHARED].join(" and ")} pass to dev`);
console.log(`  changes unfinished in the window: separate ${R.separate.unfinished}, ` +
  `shared ${R.shared.unfinished}`);

const A = [-38, 9, 13];
console.log("\n1. same set of changes in two arrangements");
print(A, "measure", "separate", "shared");
for (const [ad, k] of [["team boundary crossings", "boundaryCross"],
  ["wait per boundary (steps)", "waitPerBoundary"],
  ["wait per internal transition (steps)", "waitPerInternal"],
  ["information loss (dropped items)", "infoLoss"], ["asking cost (steps)", "ask"],
  ["changes bounced at the boundary", "bounced"], ["errors surfacing in production", "prodErrors"],
  ["rework (steps)", "rework"]]) print(A, ad, R.separate[k], R.shared[k]);

console.log("\n2. four delivery metrics");
print(A, "metric", "separate", "shared");
for (const [ad, k] of [["deployment frequency (per 100 steps)", "deployFrequency"],
  ["lead time (avg. step)", "leadTime"], ["change failure rate", "changeFailureRate"],
  ["time to restore (avg. step)", "recovery"], ["last change's entry into production", "window"]])
  print(A, ad, R.separate[k], R.shared[k]);

const B = [-25, 7, 12, 9, 13, 16];
console.log("\n3. which step an item dropped at (dropped: separate / shared)");
print(B, "step", "items", "unwritten", "dev", "silent", "dropped");
for (const a of STEP.filter((a) => a.role === "ops")) {
  const g = item.filter((k) => k.step === a.name), h = g.filter((k) => k.state !== "w");
  const d = (az) => R[az].D.flatMap((x) => x.dropped ?? []).filter((k) => k.step === a.name).length;
  print(B, a.name, g.length, h.length, h.filter((k) => k.owner === "dev").length,
    h.filter((k) => k.state === "s").length, `${d("separate")} / ${d("shared")}`);
}

const C = [-25, -10, 11, 18, 18];
console.log("\n4. path per change");
print(C, "change", "arrangement", "lead time", "boundary bounces", "production errors");
for (const [ad, f] of [["written only", (d) => d.item.every((k) => k.state === "w")],
  ["has askable", (d) => d.item.some((k) => k.state === "a") &&
    !d.item.some((k) => k.state === "s")],
  ["has silent", (d) => d.item.some((k) => k.state === "s")]])
  for (const az of ["separate", "shared"]) {
    const g = R[az].D.filter(f);
    if (!g.length) continue;
    print(C, az === "separate" ? `${ad} (${g.length})` : "", az,
      avg(g.map((d) => d.prodDone - d.i * ARRIVAL)),
      g.reduce((s, d) => s + d.bounced, 0), g.filter((d) => d.error).length);
  }
18 changes, 7 steps, 29 context items: 8 written, 14 askable, 7 silent
  where items sit: dev 19, ops 10
  separate: 4 steps sit with ops; shared: test-setup and production-verification pass to dev
  changes unfinished in the window: separate 0, shared 0

1. same set of changes in two arrangements
measure                                separate       shared
team boundary crossings                      46           42
wait per boundary (steps)                  11.2         11.0
wait per internal transition (steps)        8.1         12.2
information loss (dropped items)             14            5
asking cost (steps)                           0           12
changes bounced at the boundary              10            3
errors surfacing in production                4            2
rework (steps)                               46           17

2. four delivery metrics
metric                                 separate       shared
deployment frequency (per 100 steps)        9.5          7.9
lead time (avg. step)                      85.6         87.2
change failure rate                         22%          11%
time to restore (avg. step)                61.0         65.0
last change's entry into production         275          267

3. which step an item dropped at (dropped: separate / shared)
step                       items   unwritten      dev       silent         dropped
test-setup                     8           5        3            3           3 / 2
acceptance-test                7           4        3            0           3 / 0
production-setup               7           6        4            1           4 / 1
production-verification        7           6        4            3           4 / 2

4. path per change
change                   arrangement  lead time  boundary bounces production errors
written only (1)         separate         93.0                 0                 0
                         shared          115.0                 0                 0
has askable (10)         separate         77.2                 7                 2
                         shared           70.0                 0                 0
has silent (7)           separate         96.4                 3                 2
                         shared          107.7                 3                 2

The numbers are of the measurement kind; their inputs are the assumptions above.

What Drops at the Boundary

In the separate arrangement, fourteen items drop at the boundary: every unwritten item that sits with the development team but is needed at a step the operations team runs is lost. The result is ten boundary bounces, four production errors, and 46 steps of rework.

Under shared responsibility, dropped items fall to 5, boundary bounces to 3, production errors to 2; in exchange, 12 steps of asking cost are paid. But the third table shows the real change is a change in direction. In the separate arrangement, what drops are items that sit with the development team: 3 at test setup, 3 at acceptance test, 4 at production setup, 4 at production verification. Under shared responsibility, test setup and production verification pass to the development team, so at those steps it is now items sitting with the operations team that drop — scheduling and resource. Removing the wall does not eliminate information loss, it reverses its direction; all five of the remaining losses are things operations knows and development does not.

The fourth table sharpens the distinction. For the ten changes carrying askable context, shared responsibility brings boundary bounces from 7 to 0, production errors from 2 to 0, and lead time down from 77.2 to 70.0 steps. For the seven changes carrying silent context, nothing changes: both arrangements produce the same 3 bounces and the same 2 errors, and lead time even rises from 96.4 to 107.7 steps. Asking only works for something whose absence is known.

The Cost of Removing the Wall

The second table lays the four metrics side by side. The change failure rate falls from 22% to 11%; that is the only metric shared responsibility gains. Deployment frequency goes from 9.5 to 7.9, lead time from 85.6 to 87.2 steps, time to restore from 61.0 to 65.0 steps. One of the four metrics improves, three get worse.

The reason is in the third row of the first table: wait per internal transition rises from 8.1 to 12.2 steps. Under shared responsibility the development team runs five of the seven steps, and the bottleneck moves there. Team boundary crossings per change rise from 1 to 2, yet the total falls from 46 to 42, because bounces have fallen; wait per boundary goes from 11.2 to 11.0 steps — in other words it barely changes at all. Removing the wall does not make the boundary cheaper, it reduces how much work crosses it.

Where the Difference Hides

In this lesson, the difference hides in the context that is not written down: twenty-one of the twenty-nine items are not written — fourteen are askable, seven are silent. The declared part is only eight items. The fourteen are only visible in the run, at the step where they drop; the seven are not visible even when they drop, because no one knows the item is needed at all.

How many steps it takes the signal to reach whom also reads from here: if the dropped item is before production verification, the signal returns to the development team with one boundary crossing and an average wait of 11.2 steps; if it drops at production verification, the same signal arrives an average of 61.0 steps later, as an error that has already surfaced in production. The same missing item is more than five times as expensive, depending on where it drops.

Summary

  • Of the 29 context items carried by the same 18 changes, 21 are not written down: 14 are askable, 7 are silent.
  • In the separate arrangement, 14 items drop at the boundary, producing 10 boundary bounces and 4 production errors and writing 46 steps of rework; under shared responsibility these become 5, 3, 2, and 17, in exchange for 12 steps of asking cost.
  • Loss does not disappear, it changes direction: under shared responsibility, all five dropped items are scheduling and resource items that sit with the operations team.
  • Of the four metrics, only the change failure rate improves (22% → 11%); deployment frequency goes from 9.5 to 7.9, lead time from 85.6 to 87.2 steps, time to restore from 61.0 to 65.0 steps — the bottleneck moves to the development team (wait per internal transition 8.1 → 12.2 steps).
  • For the 10 changes carrying askable context, shared responsibility brings bounces from 7 to 0; for the 7 changes carrying silent context, the same 3 bounces and 2 errors occur under both arrangements.

Next Step

Both arrangements were measured on the same path; the path itself was held fixed. But the real question is where along that path time is spent standing still. The next lesson builds the path as a value stream map: for every step, process time, wait time, and percent complete and accurate are measured; how much of the total lead time is value-added, where the longest wait sits, and why improving one step so often fails to show up in the total are all counted.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close