Lesson 04 / 10
Scaled Frameworks
Counting the price of multi-team coordination: the same twenty-four people and twenty-four modules are split across one to twelve teams, and coordination rounds per dependent work item, the number of shared channels, the coordination layer's delay, and the cost of a joint planning round are all measured.
Contents
Every measurement in the previous lesson was made on a single team: one backlog, one WIP limit, four people. Once that assumption is removed, a new cost item is born. When a work item touches modules owned by more than one team, a coordination round has to pass between the teams; that round is neither work nor queue wait, it adds to flow time without bringing any item closer to done.
Scaled frameworks are an attempt to bring order to that round: a coordination layer, a regular joint planning round, and a list that makes cross-team dependencies visible in advance. This lesson takes up frameworks not by name but by these three elements, and it measures a single question — how much the coordination cost grows as the number of teams rises. The through-line is the regional library network, and it is fiction; the network’s twenty-four modules and twenty-four-person development roster are held fixed, and only the team boundaries change.
The Same Work, Split Differently
The block below is a model, not a measurement.
PM19 — twenty-four modules and twenty-four people are fixed; E teams only change the
boundaries. Modules are split into contiguous blocks, and each team holds 24 / E people. For the
comparison to mean anything, the roster does not grow: what is measured is not workforce, it is the
boundary itself.
PM20 — forty work items are derived from a generator; the generator is self-written and the seed is visible. Every item touches one to four modules; every touch after the first carries a dependency type — shared data, shared endpoint, shared infrastructure, or sequential delivery.
PM21 — if an item spreads across t teams, it needs t - 1 coordination rounds, and every round
costs the two teams 2 person-days. A standing channel is also opened between every pair of teams the
item touches.
PM22 — the coordination layer processes at most six rounds in one period; an item waiting its turn is delayed. The work itself, independent of coordination, takes eight periods; if coordination backs up, the period count stretches.
PM23 — at the joint planning round, every team presents and everyone listens. Its cost grows with the product of the period count, the people count, and the team count.
PM24 — in-team coordination grows with the number of person pairs: in an arrangement of E teams,
the total number of internal pairs is E · s(s-1)/2, where s = 24 / E. This is the only item that
shrinks as the number of teams grows.
// scale.mjs — the same workload split across different team counts (model) import { writeFileSync } from "node:fs"; const MODULE = 24, PEOPLE = 24; // module and people counts are fixed; only team boundaries change const ROUND_COST = 2; // cost of one coordination round to two teams (person-days) const INTERNAL_COST = 0.2; // in-team coordination cost per person pair, per period (person-days) const PLAN_COST = 0.1; // joint planning round, per person and per team (person-days) const BASE_PERIOD = 8; // the work itself, independent of coordination, takes this many periods const PERIOD_ROUNDS = 6; // rounds the coordination layer can process in one period // PM20: the generator is self-written, the seed is visible; the same item set is used at every team count. let seed = 20251103; const rand = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648; const TYPE = ["shared data", "shared endpoint", "shared infrastructure", "sequential delivery"]; const ITEM = [...Array(40)].map((no) => { const count = 1 + Math.floor(rand() * 4); const modules = [], types = []; while (modules.length < count) { const m = Math.floor(rand() * MODULE); if (!modules.includes(m)) { modules.push(m); if (modules.length > 1) types.push(TYPE[Math.floor(rand() * 4)]); } } return { no, modules, types }; }); // PM19: E teams split the modules into contiguous blocks; each team has PEOPLE / E people. const teamOf = (E) => (m) => Math.min(E - 1, Math.floor((m * E) / MODULE)); function run(E, skip = null) { const team = teamOf(E); let round = 0, dependent = 0, spread = 0; const pairs = new Set(); const coordQueue = []; // rounds entering the coordination layer, item order for (const k of ITEM) { const touches = k.modules.filter((_, i) => i === 0 || k.types[i - 1] !== skip); // the skipped type is dropped const t = [...new Set(touches.map(team))]; if (t.length <= 1) continue; dependent += 1; spread += t.length; round += t.length - 1; for (let i = 0; i < t.length; i++) for (let j = i + 1; j < t.length; j++) pairs.add(`${Math.min(t[i], t[j])}-${Math.max(t[i], t[j])}`); coordQueue.push({ no: k.no, round: t.length - 1 }); } // PM22: the coordination layer processes PERIOD_ROUNDS rounds per period; an item queued behind others is delayed. let backlog = 0, delayTotal = 0; for (const d of coordQueue) { backlog += d.round; delayTotal += Math.ceil(backlog / PERIOD_ROUNDS); } const periods = Math.max(BASE_PERIOD, Math.ceil(round / PERIOD_ROUNDS)); const peoplePerTeam = PEOPLE / E; return { E, peoplePerTeam, dependent, round, pairs: pairs.size, periods, delay: coordQueue.length ? delayTotal / coordQueue.length : 0, spread: dependent ? spread / dependent : 1, externalCost: round * ROUND_COST, planCost: +(periods * PEOPLE * E * PLAN_COST).toFixed(1), // every team presents, everyone listens internalCost: +(E * ((peoplePerTeam * (peoplePerTeam - 1)) / 2) * INTERNAL_COST * periods).toFixed(1) }; } const TEAMS = [1, 2, 3, 4, 6, 8, 12]; const SCAN = TEAMS.map((E) => { const r = run(E); return { ...r, total: +(r.externalCost + r.planCost + r.internalCost).toFixed(1) }; }); writeFileSync("scale.json", JSON.stringify({ SCAN, TYPE, skipScan: TYPE.map((t) => ({ type: t, row: TEAMS.map((E) => run(E, t).round) })) })); console.log(`modules: ${MODULE}, people: ${PEOPLE}, work items: ${ITEM.length}, ` + `dependency links: ${ITEM.reduce((a, k) => a + k.types.length, 0)} (seed ${20251103})`); console.log(`type distribution: ${TYPE.map((t) => `${t} ${ITEM.reduce((a, k) => a + k.types.filter((x) => x === t).length, 0)}`).join(", ")}`); const four = SCAN[3]; console.log(`\nfour teams, detailed: ${four.peoplePerTeam} people per team, ${MODULE / 4} modules per team`); console.log(` ${four.dependent}/${ITEM.length} items spread across more than one team, ` + `spreading items touch an average of ${four.spread.toFixed(2)} teams`); console.log(` coordination rounds ${four.round}, team pairs needing a channel ${four.pairs}/${(4 * 3) / 2}, ` + `${four.periods} periods`); console.log(` cost: external coordination ${four.externalCost}, joint planning ${four.planCost}, ` + `in-team ${four.internalCost} person-days`);
modules: 24, people: 24, work items: 40, dependency links: 69 (seed 20251103) type distribution: shared data 18, shared endpoint 13, shared infrastructure 21, sequential delivery 17 four teams, detailed: 6 people per team, 6 modules per team 31/40 items spread across more than one team, spreading items touch an average of 2.61 teams coordination rounds 50, team pairs needing a channel 6/6, 9 periods cost: external coordination 100, joint planning 86.4, in-team 108 person-days
In the four-team arrangement, thirty-one of the forty items spread across more than one team, and a spreading item touches an average of 2.61 teams. All six of the six possible team pairs end up linked because of at least one item: none of the four teams is isolated from the others.
Scanning the Team Count
// coordination.mjs — reads the scan results scale.mjs wrote; team count is scanned end to end import { readFileSync } from "node:fs"; const { SCAN, TYPE, skipScan } = JSON.parse(readFileSync("scale.json", "utf8")); 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([["team", 6], ["people/team", 13], ["dependent items", 18], ["spread", 9], ["coord. rounds", 15], ["team pairs", 12], ["delay/item", 22]], SCAN.map((r) => [r.E, r.peoplePerTeam, `${r.dependent}/40`, r.spread.toFixed(2), r.round, r.pairs, r.delay.toFixed(1)])); // PM24: the growth pattern is read through two ratios; whichever one stays flat tells the order of growth. table([["team", 6], ["coord. rounds", 15], ["rounds / team", 14], ["team pairs", 12], ["pairs / (E(E-1)/2)", 20]], SCAN.filter((r) => r.E > 1).map((r) => [r.E, r.round, (r.round / r.E).toFixed(1), r.pairs, (r.pairs / ((r.E * (r.E - 1)) / 2)).toFixed(2)]), "growth pattern:"); table([["team", 6], ["periods", 9], ["ext. coord.", 13], ["joint planning", 16], ["in-team", 10], ["total", 9]], SCAN.map((r) => [r.E, r.periods, r.externalCost, r.planCost, r.internalCost, r.total]), "coordination cost (person-days):"); const least = SCAN.reduce((a, b) => (b.total < a.total ? b : a)); console.log(`\nleast coordination cost at ${least.E} teams (${least.total} person-days); ` + `at one team ${SCAN[0].total}, at twelve teams ${SCAN[SCAN.length - 1].total} person-days`); table([["dependency type", 24], ...SCAN.map((r) => [`${r.E} teams`, 10])], skipScan.map((a) => [a.type, ...a.row.map((t, i) => t - SCAN[i].round)]), "change in coordination rounds if a dependency type is removed:"); const four = skipScan.map((a) => ({ type: a.type, gain: SCAN[3].round - a.row[3] })) .sort((a, b) => b.gain - a.gain); console.log(`\nmost rounds gained at four teams: ${four.map((d) => `${d.type} (${d.gain})`).join(", ")}`);
team people/team dependent items spread coord. rounds team pairs delay/item
1 24 0/40 1.00 0 0 0.0
2 12 27/40 2.00 27 1 2.8
3 8 30/40 2.37 41 3 3.8
4 6 31/40 2.61 50 6 4.6
6 4 32/40 2.69 54 15 4.8
8 3 33/40 2.76 58 27 5.2
12 2 34/40 2.94 66 55 5.9
growth pattern:
team coord. rounds rounds / team team pairs pairs / (E(E-1)/2)
2 27 13.5 1 1.00
3 41 13.7 3 1.00
4 50 12.5 6 1.00
6 54 9.0 15 1.00
8 58 7.3 27 0.96
12 66 5.5 55 0.83
coordination cost (person-days):
team periods ext. coord. joint planning in-team total
1 8 0 19.2 441.6 460.8
2 8 54 38.4 211.2 303.6
3 8 82 57.6 134.4 274
4 9 100 86.4 108 294.4
6 9 108 129.6 64.8 302.4
8 10 116 192 48 356
12 11 132 316.8 26.4 475.2
least coordination cost at 3 teams (274 person-days); at one team 460.8, at twelve teams 475.2 person-days
change in coordination rounds if a dependency type is removed:
dependency type 1 teams 2 teams 3 teams 4 teams 6 teams 8 teams 12 teams
shared data 0 -6 -10 -12 -15 -14 -18
shared endpoint 0 -4 -5 -8 -8 -11 -13
shared infrastructure 0 -4 -9 -11 -11 -17 -18
sequential delivery 0 -7 -11 -14 -15 -14 -17
most rounds gained at four teams: sequential delivery (14), shared data (12), shared infrastructure (11), shared endpoint (8)
Not Linear — Two Separate Patterns
The answer is not a single growth pattern, it is two separate patterns, and they read in opposite directions.
The coordination round count stays below linear. As team count rises sixfold, the round count
rises from 27 to 66; the rounds / team ratio drops from 13.5 to 5.5. The reason is a ceiling:
because an item touches at most four modules, it can spread across at most four teams, and its round
count cannot exceed three. As the team count grows, no new dependent items are found — the existing
ones just spread a little further, and spread rises from 2.00 to 2.94 and stops there.
The number of shared channels, by contrast, grows exactly quadratically. The pairs / (E(E-1)/2)
ratio is exactly 1.00 at two, three, four, and six teams: every possible team pair ends up linked
because of at least one item. Saturation only begins at eight and twelve teams (0.96 and 0.83), and
even there the pair count is 27 and 55. The coordination round count is bounded, but the channel
count those rounds spread across is not.
The cost table shows the sum of these two patterns. With one team, external coordination is zero, but internal coordination among the twenty-four people costs 441.6 person-days. With twelve teams, internal coordination falls to 26.4 person-days, but the joint planning round rises to 316.8 person-days — because every team presents and everyone listens, this item grows directly with team count. The total is lowest at three teams (274 person-days) and nearly equal at both ends: 460.8 versus 475.2. The same work, the same twenty-four people, costs twice as much by two different routes.
The coordination layer’s own delay is a separate column: it rises from 2.8 periods to 5.9 periods per dependent item. For that entire stretch, no one is working on the item.
What Removing Which Dependency Gains
The last table breaks down where the rounds come from. In the four-team arrangement, removing the sequential delivery dependency erases fourteen of the fifty rounds, shared data twelve, shared infrastructure eleven, shared endpoint eight. The order shifts with team count: at twelve teams, shared data and shared infrastructure move to the front (eighteen rounds each), and sequential delivery drops to seventeen.
What matters here is the measure, not the ranking. At four teams, eliminating a single dependency type erases between 16% and 28% of the rounds; it is not necessary to remove all four, but none of them solves the problem alone either. The cheapest improvement available without changing the team boundary is removing the dependency type that produces the most rounds from the architecture.
Summary
- The same 24 modules and 24 people were split across one to twelve teams; because the roster was held fixed, the only thing measured is the boundary itself.
- The coordination round count grows below linear (27 to 66,
rounds / teamdrops from 13.5 to 5.5), because an item touches at most four modules and its spread stops at 2.94. - Shared-channel count grows quadratically: between two and six teams every possible team pair ends
up linked (
pairs / (E(E-1)/2)exactly 1.00), and at twelve teams the pair count is 55. - Total coordination cost is lowest at three teams (274 person-days) and nearly equal at both ends — 441.6 person-days of internal coordination at one team, 316.8 person-days of joint planning at twelve teams.
- At four teams, removing a single dependency type erases 8 to 14 of the fifty rounds; sequential delivery gains the most, shared endpoint the least, and the order shifts with team count.
Next Step
Every number in this lesson stands on one assumption: when a defect is found, there is a return trip. How many rounds that return trip costs was counted, but when the defect is found was always assumed to sit in the same place — at the end of the work. The next lesson removes that assumption and closes the topic: the distance between the step a defect is born in and the step it is found in, that is, feedback delay, is measured directly. Integration frequency is scanned from once a day to once a week, pairing ratio is scanned from zero to one, and at every setting, how many steps and how many hours this delay spans, how much rework changes, and what the total work effort comes to are all counted. Pair programming’s two-person cost is factored in — having two people do one piece of work, even if it cheapens the defect found, doubles the work effort.
To keep your progress and take notes, Log in
My notes
Log in to take notes.