Skip to content
academia.sh

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 / team drops 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.

Start typing to search.

↑↓ Esc navigate · open · close