Skip to content
academia.sh

Lesson 07 / 10

Team Topologies

Measuring stream-aligned team arrangements: the same module graph and the same set of work items are run through three team arrangements — component-aligned, stream-aligned, and hybrid; cognitive load per team, external demand, duplicated expertise, and the shared component turning into a bottleneck are added alongside the handoff, waiting, and rework counts.

Contents

The previous lesson compared two team splits on the same module graph and counted the stream-aligned split shortening flow time by more than half. One column was missing from that table: the load the teams carry. To finish an item end to end, a stream-aligned team has to know not only the modules it owns but also the external interfaces it depends on.

Cognitive load is the size of what a team has to hold in mind at once to do its work; here it is built from two numbers: the number of modules the team owns, and the number of modules its modules import from outside. The same twelve work items are run through three arrangements — component-aligned, stream-aligned, and hybrid. The through-line is the regional library network and it is fictional.

A Third Arrangement and a Load Measure

TD8 — the previous lesson’s seventeen-module graph, its two splits, and its twelve work items are used unchanged. Rationale: three arrangements can only be compared over the same graph and the same workload.

TD9 — the hybrid arrangement has seven teams: three stream teams, one interface, one rule, one bridge, and one platform team; there are twelve channels. Rationale: the hybrid arrangement is an attempt to give domain knowledge to the streams and technical knowledge to a shared team; because the team count grows, so does the channel count.

TD10 — a team’s cognitive load is the number of modules it owns plus the number of distinct modules its modules import from outside. TD11 — a team’s external demand is the number of items opened by another team that touch that team’s modules. Rationale: load measures what a team knows, external demand measures the work that comes to it from someone else; in one team the two can point in opposite directions.

// layout.mjs — the same module graph and work items run through three team arrangements (model)
import { writeFileSync } from "node:fs";

// TD8 — the previous lesson's 17-module graph, its two splits, and its 12 work items are used unchanged
const IMPORTS = {
  branchFront: "loanFlow feeFlow reservationFlow sharedFormat",
  staffFront: "membershipFlow loanFlow feeFlow sharedFormat",
  mobileAccess: "loanFlow reservationFlow catalogBridge sharedFormat",
  loanFlow: "loanRule catalogRule storeAccess notificationQueue sharedFormat",
  feeFlow: "feeRule storeAccess notificationQueue sharedFormat",
  membershipFlow: "membershipRule identityBridge storeAccess sharedFormat",
  reservationFlow: "loanRule catalogBridge storeAccess notificationQueue sharedFormat",
  loanRule: "sharedFormat", feeRule: "loanRule sharedFormat",
  catalogRule: "sharedFormat", membershipRule: "sharedFormat",
  storeAccess: "eventLog sharedFormat", catalogBridge: "catalogRule sharedFormat",
  identityBridge: "sharedFormat", notificationQueue: "eventLog sharedFormat",
  eventLog: "sharedFormat", sharedFormat: "",
};
const M = Object.keys(IMPORTS);
const EDGES = Object.entries(IMPORTS)
  .flatMap(([a, s]) => (s ? s.split(" ").map((b) => [a, b]) : []));
const ITEMS = [
  ["late fee rate", "feeFlow feeRule branchFront sharedFormat"],
  ["reservation cancellation", "mobileAccess reservationFlow loanRule notificationQueue"],
  ["membership reminder", "membershipFlow membershipRule notificationQueue staffFront"],
  ["catalog record field", "catalogBridge catalogRule sharedFormat mobileAccess"],
  ["loan period extension", "loanFlow loanRule branchFront"],
  ["second identity step", "identityBridge membershipFlow staffFront sharedFormat"],
  ["fee refund record", "feeFlow storeAccess eventLog"],
  ["branch delay report", "staffFront storeAccess loanFlow"],
  ["notification text format", "notificationQueue sharedFormat"],
  ["reservation queue", "mobileAccess reservationFlow branchFront"],
  ["loan rule exception", "loanRule feeRule loanFlow feeFlow"],
  ["event log field", "eventLog sharedFormat storeAccess"],
];

const LAYOUTS = [
  { name: "component-aligned", teams: {                     // TD8: the previous lesson's split A
    interface: "branchFront staffFront mobileAccess",
    flow: "loanFlow feeFlow membershipFlow reservationFlow",
    rule: "loanRule feeRule catalogRule membershipRule",
    infra: "storeAccess catalogBridge identityBridge notificationQueue eventLog",
    shared: "sharedFormat" },
    channels: "interface-flow flow-rule flow-infra rule-infra" },
  { name: "stream-aligned", teams: {                        // TD8: the previous lesson's split B
    experience: "branchFront staffFront mobileAccess",
    loan: "loanFlow loanRule reservationFlow storeAccess",
    fee: "feeFlow feeRule",
    membership: "membershipFlow membershipRule identityBridge",
    catalog: "catalogRule catalogBridge",
    platform: "sharedFormat eventLog notificationQueue" },
    channels: "experience-loan experience-fee experience-membership loan-fee loan-catalog " +
      "loan-platform fee-platform membership-platform catalog-platform" },
  { name: "hybrid", teams: {                                // TD9: stream teams + shared component teams
    experience: "branchFront staffFront mobileAccess",
    loan: "loanFlow reservationFlow",
    fee: "feeFlow", membership: "membershipFlow",
    rule: "loanRule feeRule catalogRule membershipRule",
    bridge: "storeAccess catalogBridge identityBridge",
    platform: "sharedFormat eventLog notificationQueue" },
    channels: "experience-loan experience-fee experience-membership loan-rule fee-rule membership-rule " +
      "loan-bridge membership-bridge rule-bridge rule-platform bridge-platform experience-platform" },
];

function build({ name, teams, channels }) {
  const owner = {}, T = Object.keys(teams), adj = Object.fromEntries(T.map((e) => [e, []]));
  for (const [e, ms] of Object.entries(teams)) for (const m of ms.split(" ")) owner[m] = e;
  for (const k of channels.split(" ")) {
    const [x, y] = k.split("-");
    adj[x].push(y); adj[y].push(x);
  }
  const d = {};
  for (const s of T) {
    d[s] = Object.fromEntries(T.map((e) => [e, -1]));
    d[s][s] = 0;
    for (let q = [s]; q.length; ) {
      const u = q.shift();
      for (const v of adj[u]) if (d[s][v] < 0) { d[s][v] = d[s][u] + 1; q.push(v); }
    }
  }
  const contributors = Object.fromEntries(M.map((m) => [m, new Set([owner[m]])]));
  for (const [, ms] of ITEMS) for (const m of ms.split(" ")) contributors[m].add(owner[ms.split(" ")[0]]);
  // TD10 — cognitive load: modules owned + externally imported modules (external interface)
  const load = Object.fromEntries(T.map((e) => [e, { owned: teams[e].split(" ").length,
    extInterface: new Set(), extTeam: new Set(), extDemand: 0 }]));
  for (const [a, b] of EDGES) if (owner[a] !== owner[b]) {
    load[owner[a]].extInterface.add(b);
    load[owner[a]].extTeam.add(owner[b]);
  }
  // TD11 — external demand: an item opened by another team that touches this team's modules
  for (const [, ms] of ITEMS) {
    const dizi = ms.split(" "), bas = owner[dizi[0]];
    for (const e of new Set(dizi.map((m) => owner[m]))) if (e !== bas) load[e].extDemand += 1;
  }
  return { name, channelCount: channels.split(" ").length, teams, owner, d, contributors, load, T };
}

const D = LAYOUTS.map(build);
writeFileSync("layout.json", JSON.stringify({ M, EDGES, ITEMS, D: D.map((x) =>
  ({ ...x, contributors: Object.fromEntries(M.map((m) => [m, [...x.contributors[m]]])) })) }));

const spread = (v) => `${v[0]} / ${v[(v.length - 1) >> 1]} / ${v[v.length - 1]}`;
console.log(`${"layout".padEnd(19)}${"teams".padStart(6)}${"channels".padStart(9)}` +
  `${"modules owned".padStart(22)}${"cognitive load".padStart(22)}`);
console.log(`${"".padEnd(34)}${"low/median/high".padStart(22)}${"low/median/high".padStart(22)}`);
for (const x of D) {
  const owned = x.T.map((e) => x.load[e].owned).sort((a, b) => a - b);
  const ld = x.T.map((e) => x.load[e].owned + x.load[e].extInterface.size).sort((a, b) => a - b);
  console.log(`${x.name.padEnd(19)}${String(x.T.length).padStart(6)}` +
    `${String(x.channelCount).padStart(9)}${spread(owned).padStart(22)}${spread(ld).padStart(22)}`);
}

for (const x of D) {
  console.log(`\n${x.name} — cognitive load per team`);
  console.log(`${"team".padEnd(12)}${"owned".padStart(7)}${"ext iface".padStart(11)}` +
    `${"ext team".padStart(10)}${"load".padStart(6)}${"ext demand".padStart(12)}`);
  for (const e of x.T)
    console.log(`${e.padEnd(12)}${String(x.load[e].owned).padStart(7)}` +
      `${String(x.load[e].extInterface.size).padStart(11)}${String(x.load[e].extTeam.size).padStart(10)}` +
      `${String(x.load[e].owned + x.load[e].extInterface.size).padStart(6)}` +
      `${String(x.load[e].extDemand).padStart(12)}`);
}
layout              teams channels         modules owned        cognitive load
                                         low/median/high       low/median/high
component-aligned       5        4             1 / 4 / 5            1 / 7 / 13
stream-aligned          6        9             2 / 3 / 4             3 / 5 / 9
hybrid                  7       12             1 / 3 / 4             3 / 5 / 9

component-aligned — cognitive load per team
team          owned  ext iface  ext team  load  ext demand
interface         3          6         3     9           5
flow              4          9         3    13           5
rule              4          1         1     5           5
infra             5          2         2     7           4
shared            1          0         0     1           5

stream-aligned — cognitive load per team
team          owned  ext iface  ext team  load  ext demand
experience        3          6         5     9           5
loan              4          5         2     9           5
fee               2          4         2     6           1
membership        3          2         2     5           0
catalog           2          1         1     3           0
platform          3          0         0     3           6

hybrid — cognitive load per team
team          owned  ext iface  ext team  load  ext demand
experience        3          6         5     9           5
loan              2          6         3     8           4
fee               1          4         3     5           1
membership        1          4         3     5           1
rule              4          1         1     5           5
bridge            3          3         2     6           3
platform          3          0         0     3           6

How the Load Is Distributed

The first table lays out the component-aligned arrangement’s problem: load ranges from a low of 1 to a high of 13. The flow team owns four modules but depends on nine external interfaces; the shared team owns a single module and depends on no external interface at all. In the same organization, one team has to know thirteen things and another has to know one. In the stream-aligned and hybrid arrangements the range narrows to 3–9.

How the balancing happens shows up in the second table. The flow team’s nine external interfaces split across three teams: loan depends on five, fee on four, membership on two. The load did not vanish, it split — and the split works, because each stream team also owns the rule for its own domain: the loan team keeps loanRule in-house, and that module no longer counts as an external interface.

The external demand column reads in the opposite direction. In the stream-aligned arrangement, the platform team’s cognitive load is three — one of the lowest values — but its external demand is six. The team with the lightest load is the team the work passes through the most.

Flow Numbers and Duplicated Expertise

The second run compares the three arrangements in terms of flow and adds two new numbers.

TD12 — a module’s duplicated expertise is the number of teams that import it without owning it. Rationale: every team that imports a module has to learn its interface; the more teams the same knowledge is repeated in, the more duplicated the expertise.

TD13 — the bottleneck is the team receiving the most external demand; the rework items touching that team’s modules are counted separately. Rationale: where the cost accumulates can only be seen by reading these two numbers together.

// flow2.mjs — flow numbers across three layouts, and duplicated expertise
import { readFileSync } from "node:fs";
const { M, EDGES, ITEMS, D } = JSON.parse(readFileSync("layout.json", "utf8"));

const WAIT = [0, 1, 3, 5], NONE = 8;      // TD6's wait table is used unchanged
const run = (x) => ITEMS.map(([name, mods]) => {
  const ms = mods.split(" "), order = [...new Set(ms.map((m) => x.owner[m]))];
  let wait = 0, longest = 0, broken = 0;
  for (let j = 1; j < order.length; j++) {
    const u = x.d[order[j - 1]][order[j]], b = u < 0 ? NONE : WAIT[u];
    wait += b; longest = Math.max(longest, b);
    if (u < 0 || u >= 2) broken += 1;
  }
  const reasons = [];
  if (broken) reasons.push("late");
  if (ms.some((m) => x.contributors[m].length >= 3)) reasons.push("bound");
  if (order.length >= 4) reasons.push("info");
  const work = ms.length + reasons.length * 2, waitT = wait + reasons.length * longest;
  return { name, team: order.length, hnd: order.length - 1, work, wait: waitT, flow: work + waitT, reasons };
});

const R = D.map((x) => ({ name: x.name, k: run(x) }));
const T = (r, f) => r.k.reduce((t, y) => t + f(y), 0);
const n = (r, c) => r.k.filter((y) => y.reasons.includes(c)).length;
console.log(`${"layout".padEnd(19)}${"hnd.".padStart(6)}${"wait".padStart(6)}` +
  `${"flow".padStart(6)}${"wait share".padStart(13)}${"rework".padStart(8)}` +
  `${"late".padStart(6)}${"bound".padStart(7)}${"info".padStart(6)}${"1-team".padStart(8)}`);
for (const r of R)
  console.log(`${r.name.padEnd(19)}${String(T(r, (y) => y.hnd)).padStart(6)}` +
    `${String(T(r, (y) => y.wait)).padStart(6)}${String(T(r, (y) => y.flow)).padStart(6)}` +
    `${`${(100 * T(r, (y) => y.wait) / T(r, (y) => y.flow)).toFixed(1)}%`.padStart(13)}` +
    `${String(T(r, (y) => y.reasons.length)).padStart(8)}${String(n(r, "late")).padStart(6)}` +
    `${String(n(r, "bound")).padStart(7)}${String(n(r, "info")).padStart(6)}` +
    `${String(r.k.filter((y) => y.team === 1).length).padStart(8)}`);

// TD12 — duplicated expertise: the number of teams that import a module without owning it
const dependent = (x, m) => new Set(EDGES.filter(([a, b]) => b === m && x.owner[a] !== x.owner[m])
  .map(([a]) => x.owner[a])).size;
const relevant = M.filter((m) => D.some((x) => dependent(x, m) >= 2));
console.log(`\n${"module".padEnd(18)}${D.map((x) => x.name.padStart(19)).join("")}   owner (three layouts)`);
for (const m of relevant)
  console.log(`${m.padEnd(18)}${D.map((x) => String(dependent(x, m)).padStart(19)).join("")}` +
    `   ${D.map((x) => x.owner[m]).join(" / ")}`);
console.log(`${"TOTAL team-module".padEnd(18)}` +
  `${D.map((x) => String(M.reduce((t, m) => t + dependent(x, m), 0)).padStart(19)).join("")}`);

// TD13 — bottleneck: the team with the most external demand and the rework items touching its modules
console.log(`\n${"layout".padEnd(19)}${"team with most external demand".padStart(31)}` +
  `${"ext demand".padStart(11)}${"owned".padStart(7)}${"rework via its modules".padStart(24)}`);
D.forEach((x, i) => {
  const e = x.T.reduce((a, b) => (x.load[a].extDemand >= x.load[b].extDemand ? a : b));
  const hit = R[i].k.filter((y, j) => y.reasons.length &&
    ITEMS[j][1].split(" ").some((m) => x.owner[m] === e)).length;
  console.log(`${x.name.padEnd(19)}${e.padStart(31)}${String(x.load[e].extDemand).padStart(11)}` +
    `${String(x.load[e].owned).padStart(7)}${String(hit).padStart(24)}`);
});
layout               hnd.  wait  flow   wait share  rework  late  bound  info  1-team
component-aligned      24   203   292        69.5%      24     8     11     5       0
stream-aligned         17    55   126        43.7%      15     4     11     0       1
hybrid                 25    78   163        47.9%      22     5     12     5       1

module              component-aligned     stream-aligned             hybrid   owner (three layouts)
catalogRule                         2                  1                  2   rule / catalog / rule
storeAccess                         1                  2                  3   infra / loan / bridge
catalogBridge                       2                  2                  2   infra / catalog / bridge
notificationQueue                   1                  2                  2   infra / platform / platform
sharedFormat                        4                  5                  6   shared / platform / platform
TOTAL team-module                  18                 18                 24

layout              team with most external demand ext demand  owned  rework via its modules
component-aligned                        interface          5      3                       7
stream-aligned                            platform          6      3                       8
hybrid                                    platform          6      3                       8

Where the Stream-Aligned Arrangement Loses

In the flow numbers, the stream-aligned arrangement leads all three: the fewest handoffs (17), the least waiting (55 rounds), the shortest flow (126 rounds), the least rework (15). The hybrid arrangement does the opposite of what one would expect — with twenty-five handoffs it is the highest of the three arrangements, exceeding the component-aligned arrangement’s twenty-four as well. The reason is structural: the hybrid arrangement splits the flow, but it also gives the rule, the bridge, and the platform to separate teams, so an item crosses both the stream boundary and the layer boundary. It is also highest in the bound reason (12).

Where the stream-aligned arrangement loses is in the second table. The total team-module dependency pair count is 18 in the component-aligned arrangement and 18 in the stream-aligned arrangement as well — the stream-aligned arrangement does not reduce duplicated expertise at all. Row by row, though, the distribution shifts: storeAccess is imported from outside by two teams instead of one, notificationQueue by two instead of one, sharedFormat by five teams instead of four. The number of teams that have to know the shared format module has grown, because every stream team runs into that module’s interface while finishing its own item end to end. In the hybrid arrangement the total climbs to 24 and sharedFormat reaches six teams: the more an arrangement is split, the more duplicated expertise grows.

The third table names what the shared component is. In the stream-aligned and hybrid arrangements the bottleneck is the platform team: it owns three modules, receives six external demands, and eight of the items entering rework touch one of its modules. The shared component is not ownerless — it has an owner — but it produces no item of its own stream; it only makes someone else’s item wait. In the component-aligned arrangement the same role is played by the interface team (five external demands, seven rework items), but there the bottleneck’s load is also high (9); in the stream-aligned arrangement it is three. As the arrangement changes, the bottleneck’s location changes; its existence does not.

The stream-aligned arrangement improves the flow numbers and the load balance together; it does this without reducing duplicated expertise, and even increases it in the shared modules. The hybrid arrangement balances the load by the same measure, but pushes handoffs and duplicated expertise to their worst point.

Summary

  • The same graph and the same 12 items were run through three arrangements: component-aligned (5 teams, 4 channels), stream-aligned (6 teams, 9 channels), and hybrid (7 teams, 12 channels).
  • In the component-aligned arrangement, cognitive load spreads from 1 to 13; the flow team owns 4 modules while depending on 9 external interfaces. In the stream-aligned and hybrid arrangements, the range narrows to 3–9.
  • In the flow numbers, the stream-aligned arrangement leads: 17 handoffs, 55 rounds of waiting, 126 rounds of flow, 15 rework items. The hybrid arrangement, with 25 handoffs, is the highest of the three, exceeding the component-aligned arrangement’s 24 as well.
  • The stream-aligned arrangement gains nothing in duplicated expertise: the total team-module dependency pair count is 18 in both the component-aligned and the stream-aligned arrangements, and 24 in the hybrid arrangement.
  • The number of teams importing sharedFormat from outside is 4, 5, and 6 across the three arrangements in order; the more the arrangement is split, the more teams have to know the shared format.
  • In the stream-aligned and hybrid arrangements the bottleneck is the platform team: its cognitive load is 3, its external demand is 6, and 8 of the items entering rework touch its modules. The bottleneck’s location changes; its existence does not.

Next Step

These two lessons measured where the team boundary falls and ran a quarter’s worth of workload through three arrangements. In every run, the modules a work item would touch were known from the start. In reality this information does not exist when an item begins: how long an item will take can only be estimated, and the estimate narrows as work proceeds. The next lesson measures estimation — a set of estimates is compared against the durations that actually occurred, the margin of error and the stage-by-stage narrowing of uncertainty are counted, and it is shown why a single-item estimate and a total estimate behave differently.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close