---
title: 'Team Topologies'
source: 'https://academia.sh/en/courses/process-and-team/team-topologies'
course: 'Process, Team and Delivery'
language: en
updated: '2026-08-23T07:01:08+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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.

```js
// 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.
