Skip to content
academia.sh

Lesson 07 / 11

The Layered Diagram Approach

Modeling the same system at the context, container, component, and code level: the number of nodes and edges per level, the level at which a question gets answered, the unnecessary nodes a reader looking at the wrong level reads, and the break-even point of keeping four levels in maintenance.

Contents

The previous lesson split the system into four views along its axis of concern and counted the questions each view answered. One axis stayed uncut: detail. Someone asking what the regional library network exchanges with the outside and someone asking what the late-penalty formula calls both have to look at the same module list. For the first, the list is too crowded; for the second, too coarse.

The layered diagram approach cuts this axis. The same system is modeled at four scale levels: the context level shows the system as a single box and its relationship with outside parties; the container level shows the separately running, separately deployed units inside the system; the component level shows the responsibility parts inside a container; the code level shows the functions inside a component. The levels do not show different things — they show the same thing at a different scale.

Let the boundary between the two terms be written down. A container is a separately running, separately deployed unit — a front-end application, a service, a data store, a message bus. Component was established earlier as a responsibility part inside a module; the difference here is context: there it was a separable unit inside a codebase, here it is a separable unit inside a container and is not deployed on its own.

Model

No diagram is drawn. The structure underneath the drawing is modeled: nodes, edges, and the tree connecting the nodes to each other. The tree is hand-written (VM5), and so are the component-level dependencies (VM6). The code-level edges are derived from both: each component dependency is realized at the code level by a single pair of items (VM7), and each component’s items are chained together internally (VM8). The seed is visible.

The upper levels’ edges are computed, not written: both ends of a code edge are replaced by their ancestors at the desired level, self-loops are discarded, and the remainder is deduplicated. This is called edge promotion. The method’s correctness is a measurable claim and is tested in the output: the hand-written component edges must come back when promoted from the code edges.

The question set is once again a data structure (VM9). Each question has a target node and an answer level: the shallowest level at which the target’s neighbors can still be told apart. The reading cost is counted like this: the nodes read at the answer level are the target plus its neighbors. Someone reading at a deeper level has to read all the descendant nodes of the target and its neighbors. At a shallower level, the target disappears inside its own ancestor and the question stays unanswered.

// views/scale.mjs — MODEL regional library network at four scale levels: context,
// container, component, code. Not a real institution; tree and component edges are hand-written.

// TREE — container > component > code item (VM5)
const TREE = {
  "web-portal": { "session-front": ["login-flow", "session-cookie", "password-reset"],
    "search-front": ["query-form", "result-list", "filter"], "account-front": ["loan-list", "penalty-summary"] },
  "staff-app": { "counter-front": ["checkout-screen", "return-screen", "penalty-screen"],
    "admin-front": ["member-management", "report-screen"] },
  "kiosk-front": { "kiosk-interface": ["card-read", "quick-loan"], "kiosk-cache": ["local-catalog", "local-duration"] },
  "loan-service": { "loan-core": ["open-loan", "take-return", "extension"],
    "loan-rule": ["duration-calc", "limit-check", "late-penalty"], reservation: ["reservation-queue", "reservation-notification"] },
  "member-service": { "member-registry": ["create-member", "update-member", "card-link"], "member-penalty": ["penalty-accrual", "penalty-collection"] },
  "catalog-integration": { "catalog-connector": ["external-query", "record-conversion", "retry"],
    "catalog-cache": ["cache-key", "expiry"] },
  "inventory-service": { "branch-inventory": ["copy-status", "shelf-location"], "branch-sync": ["sync-job", "conflict-resolution"] },
  "notification-service": { "notification-queue": ["queue-write", "queue-read"], "notification-email": ["message-body", "send-attempt"] },
  "report-service": { "report-daily": ["daily-total", "branch-breakdown"], "report-batch": ["monthly-summary", "archive-write"] },
  "main-database": { "relational-schema": ["member-table", "copy-table", "loan-table", "penalty-table"] },
  "event-bus": { "topic-routing": ["topic-definition", "subscription"] },
  cache: { "key-value": ["write", "read", "timed-delete"] },
};
const SYSTEM = "library-network";
const EXTERNAL = ["member", "staff", "external-catalog", "email-gateway", "identity-provider"];

// COMPONENT_EDGE — component-level dependencies (VM6); chosen by hand
const COMPONENT_EDGE = [
  ["member", "search-front"], ["member", "session-front"], ["member", "account-front"], ["member", "kiosk-interface"],
  ["staff", "counter-front"], ["staff", "admin-front"],
  ["session-front", "identity-provider"], ["session-front", "member-registry"],
  ["search-front", "catalog-cache"], ["search-front", "branch-inventory"],
  ["account-front", "loan-core"], ["account-front", "member-penalty"],
  ["counter-front", "loan-core"], ["counter-front", "member-penalty"], ["counter-front", "branch-inventory"],
  ["admin-front", "member-registry"], ["admin-front", "report-daily"],
  ["kiosk-interface", "loan-core"], ["kiosk-interface", "kiosk-cache"], ["kiosk-cache", "catalog-cache"],
  ["loan-core", "loan-rule"], ["loan-core", "relational-schema"],
  ["loan-core", "topic-routing"], ["loan-core", "branch-inventory"],
  ["reservation", "loan-core"], ["reservation", "notification-queue"], ["reservation", "relational-schema"],
  ["member-registry", "relational-schema"],
  ["member-penalty", "member-registry"], ["member-penalty", "loan-rule"], ["member-penalty", "relational-schema"],
  ["catalog-connector", "external-catalog"], ["catalog-cache", "catalog-connector"], ["catalog-cache", "key-value"],
  ["branch-inventory", "relational-schema"], ["branch-inventory", "topic-routing"],
  ["branch-sync", "branch-inventory"], ["branch-sync", "topic-routing"], ["branch-sync", "catalog-connector"],
  ["notification-queue", "topic-routing"], ["notification-queue", "relational-schema"],
  ["notification-email", "notification-queue"], ["notification-email", "email-gateway"],
  ["report-daily", "relational-schema"], ["report-batch", "relational-schema"], ["report-batch", "branch-inventory"],
];

// ---- mappings derived from the tree ----
const CONTAINERS = Object.keys(TREE);
const PARENT = { component: {}, container: {} }; // code->component, component->container
for (const k of CONTAINERS) for (const b of Object.keys(TREE[k])) {
  PARENT.container[b] = k;
  for (const c of TREE[k][b]) PARENT.component[c] = b;
}
const COMPONENTS = Object.keys(PARENT.container);
const CODE_ITEMS = Object.keys(PARENT.component);
const LEVEL = ["context", "container", "component", "code"];
const isExternal = (n) => EXTERNAL.includes(n);
const levelOf = (n) => (isExternal(n) ? -1 : n === SYSTEM ? 0 : CONTAINERS.includes(n) ? 1 : COMPONENTS.includes(n) ? 2 : 3);
const represents = (n, L) => { let m = n; while (!isExternal(m) && levelOf(m) > L) m = levelOf(m) === 3 ? PARENT.component[m] : levelOf(m) === 2 ? PARENT.container[m] : SYSTEM; return m; };
const CONTENT = { context: [SYSTEM], container: CONTAINERS, component: COMPONENTS, code: CODE_ITEMS };
const nodesAt = (L) => [...CONTENT[LEVEL[L]], ...EXTERNAL];
const descendantsAt = (n, L) => (levelOf(n) >= L || isExternal(n) ? [represents(n, L)] : nodesAt(L).filter((m) => represents(m, levelOf(n)) === n));

// ---- code-level edges: each component edge is realized by one item pair (VM7),
// each component's items are chained together internally (VM8) ----
function prng(seed) { let s = seed >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32; }
const rand = prng(31607); // seed is visible
const codeItem = (n) => (isExternal(n) ? n : TREE[PARENT.container[n]][n][Math.floor(rand() * TREE[PARENT.container[n]][n].length)]);
const CODE_EDGE = COMPONENT_EDGE.map(([a, b]) => [codeItem(a), codeItem(b)]);
for (const b of COMPONENTS) { const g = TREE[PARENT.container[b]][b]; for (let i = 1; i < g.length; i++) CODE_EDGE.push([g[i - 1], g[i]]); }

const edgesAt = (L) => [...new Set(CODE_EDGE.map(([a, b]) => [represents(a, L), represents(b, L)])
  .filter(([a, b]) => a !== b).map(([a, b]) => `${a}->${b}`))].map((s) => s.split("->"));

// ---- nodes and edges per level ----
const col = (s, n) => String(s).padEnd(n);
console.log(col("level", 10) + col("nodes", 8) + col("edges", 8) + "internal nodes + external parties");
console.log("-".repeat(52));
LEVEL.forEach((d, L) => console.log(col(d, 10) + col(nodesAt(L).length, 8) + col(edgesAt(L).length, 8) + `${CONTENT[d].length} + ${EXTERNAL.length}`));

const recovered = new Set(edgesAt(2).map((e) => e.join("->")));
const handWritten = COMPONENT_EDGE.map((e) => e.join("->"));
console.log(`\n${handWritten.filter((e) => recovered.has(e)).length} of the ${handWritten.length} hand-written component edges come back when promoted from code edges`);

// ---- question set: each question's target and answer level (VM9) ----
const QUESTION = [
  { id: "D01", text: "which outside parties does the network talk to", target: SYSTEM, level: 0 },
  { id: "D02", text: "is the identity provider inside or outside the system", target: "identity-provider", level: 0 },
  { id: "D03", text: "which containers does the loan service talk to", target: "loan-service", level: 1 },
  { id: "D04", text: "which container connects to the email gateway", target: "email-gateway", level: 1 },
  { id: "D05", text: "which containers write to the main database", target: "main-database", level: 1 },
  { id: "D06", text: "which containers use the event bus", target: "event-bus", level: 1 },
  { id: "D07", text: "what does the loan-rule component depend on", target: "loan-rule", level: 2 },
  { id: "D08", text: "which components read the catalog cache", target: "catalog-cache", level: 2 },
  { id: "D09", text: "which components does the penalty calculation touch", target: "member-penalty", level: 2 },
  { id: "D10", text: "which component writes to the notification queue", target: "notification-queue", level: 2 },
  { id: "D11", text: "what does the late-penalty calculation call", target: "late-penalty", level: 3 },
  { id: "D12", text: "which item does the duration calculation depend on", target: "duration-calc", level: 3 },
  { id: "D13", text: "what does the extension function touch", target: "extension", level: 3 },
  { id: "D14", text: "where is the cache key generated", target: "cache-key", level: 3 },
];

const neighbors = (n, L) => { const t = represents(n, L); return [...new Set(edgesAt(L).filter((e) => e.includes(t)).flat())].filter((m) => m !== t); };
const nodesRead = (q, L) => (L < q.level ? null : L === q.level ? 1 + neighbors(q.target, L).length
  : descendantsAt(q.target, L).length + neighbors(q.target, q.level).reduce((t, m) => t + descendantsAt(m, L).length, 0));

console.log("\nquestion  answer level  nodes read    at code level  extra");
console.log("-".repeat(60));
let minimum = 0, allCode = 0;
for (const q of QUESTION) {
  const min = nodesRead(q, q.level), code = nodesRead(q, 3);
  minimum += min; allCode += code;
  console.log(col(q.id, 10) + col(LEVEL[q.level], 14) + col(min, 14) + col(code, 15) + (code - min));
}
console.log("-".repeat(60));
console.log(col("total", 24) + col(minimum, 14) + col(allCode, 15) + (allCode - minimum));

console.log("\nthe same question read at all four levels:");
console.log((col("question", 10) + LEVEL.map((d) => col(d, 10)).join("")).trimEnd());
for (const id of ["D01", "D03", "D11"]) {
  const q = QUESTION.find((x) => x.id === id);
  console.log((col(q.id, 10) + LEVEL.map((d, L) => col(nodesRead(q, L) ?? "no answer", 10)).join("")).trimEnd());
}

console.log("\nanswer level distribution: " + LEVEL.map((d, L) => `${d}=${QUESTION.filter((q) => q.level === L).length}`).join(", "));
LEVEL.forEach((d, L) => {
  const answered = QUESTION.filter((q) => q.level <= L);
  const unanswered = QUESTION.length - answered.length;
  console.log(`${col("only " + d, 22)}answered ${col(`${answered.length}/${QUESTION.length}`, 8)}unanswered ${col(unanswered, 4)}reading ${answered.reduce((t, q) => t + nodesRead(q, L), 0)}`);
});

// ---- maintenance and break-even ----
// VM10: reading a node and keeping a node current are counted as equal cost
const maintFour = LEVEL.reduce((t, d, L) => t + nodesAt(L).length, 0);
const maintCode = nodesAt(3).length;
const gain = allCode - minimum;
console.log(`\nall four levels keep ${maintFour} nodes in maintenance, code level alone keeps ${maintCode}; difference ${maintFour - maintCode}`);
console.log(`one round of questions saves ${gain} unnecessary node reads (${allCode} instead of ${minimum})`);
console.log(`break-even (VM10): if the question set is asked once every ${(gain / (maintFour - maintCode)).toFixed(2)} change rounds, the extra maintenance of four levels pays for itself`);
level     nodes   edges   internal nodes + external parties
----------------------------------------------------
context   6       5       1 + 5
container 17      30      12 + 5
component 28      46      23 + 5
code      61      79      56 + 5

46 of the 46 hand-written component edges come back when promoted from code edges

question  answer level  nodes read    at code level  extra
------------------------------------------------------------
D01       context       6             61             55
D02       context       2             57             55
D03       container     9             44             35
D04       container     2             5              3
D05       container     6             29             23
D06       container     4             18             14
D07       component     3             8              5
D08       component     5             13             8
D09       component     6             17             11
D10       component     5             12             7
D11       code          3             3              0
D12       code          3             3              0
D13       code          5             5              0
D14       code          3             3              0
------------------------------------------------------------
total                   62            278            216

the same question read at all four levels:
question  context   container component code
D01       6         17        28        61
D03       no answer 9         18        44
D11       no answer no answer no answer 3

answer level distribution: context=2, container=4, component=4, code=4
only context          answered 2/14    unanswered 12  reading 8
only container        answered 6/14    unanswered 8   reading 51
only component        answered 10/14   unanswered 4   reading 112
only code             answered 14/14   unanswered 0   reading 278

all four levels keep 112 nodes in maintenance, code level alone keeps 61; difference 51
one round of questions saves 216 unnecessary node reads (278 instead of 62)
break-even (VM10): if the question set is asked once every 4.24 change rounds, the extra maintenance of four levels pays for itself

The Size of the Levels

The four levels’ node counts are 6, 17, 28, and 61; the edge counts are 5, 30, 46, and 79. The context level consists of nothing more than one system box and five outside parties; the code level has fifty-six internal nodes. The growth between scale levels is roughly a doubling each time, and this is a deliberate design: a reader moving from one level to the next doubles the number of nodes they see at each step, not multiplies it by fifty.

The promotion method’s correctness is tested in the output’s second line: all forty-six of the forty-six hand-written component edges come back when promoted from the code edges. The upper levels are not independent documents; they’re the same graph read at a coarse resolution — as long as they’re derived, they cannot contradict the level below; when they’re hand-written, they can.

At Which Level a Question Gets Answered

The fourteen questions are not split equally across the four levels: two find their answer at the context level, four at the container level, four at the component level, four at the code level. The total reading cost, with every question read at its own answer level, is 62 nodes. This is the lesson’s zero point: the minimum reading needed to answer all fourteen questions.

If the same fourteen questions were answered by looking only at the code level, the number of nodes read would be 278. The difference is 216 unnecessary nodes — nodes read that contribute nothing to the answer, read only because the right scale was not at hand. The first two questions are the most expensive: someone asking about the network’s relationship with the outside reads six nodes at the context level, sixty-one at the code level. Fifty-five nodes are detail that does not change the answer.

For the four questions whose answer level is code, the unnecessary-node count is zero; the layered approach gains them nothing. The gain grows with the distance between a question’s answer level and the level the reader is looking at.

The Two Directions of the Wrong Level

The “same question read at all four levels” table shows the cost for a single question. D01, which asks about the network’s external relationships, reads 6 nodes at the context level, 17 at the container level, 28 at the component level, 61 at the code level — the answer can be found at every level, but its cost climbs tenfold. D03, which asks about the loan service’s neighbors, is unanswered at the context level: there’s no node called the loan service at that level, it stays inside the system box. D11, which asks about the late-penalty calculation, is unanswered at all three of the first levels.

Both directions produce a number, and the numbers are not symmetric. Looking too deep gives an answer but reads unnecessary nodes; it is a measurable, tolerable waste. Looking too high gives no answer at all; the reader either gives up or guesses. The table below counts this at the scheme level: a document that keeps only the context level answers two of the fourteen questions, twelve stay unanswered. The container level alone answers six, the component level alone answers ten. The code level alone answers all of them but reads 278 nodes.

The Cost of Maintenance

Keeping all four levels means keeping 112 nodes in maintenance; keeping only the code level means 61. The difference is 51 nodes, and this difference is not free: every node at the container and component level has to be updated whenever its counterpart on the code side changes.

What’s gained in return is 216 unnecessary node reads saved per round of questions. Because the two quantities are in different units, an assumption is needed: reading a node and keeping a node current are counted as equal cost (VM10). Under this assumption, the break-even point is this: if the question set is asked once every 4.24 change rounds, the extra maintenance of the four levels pays for itself. If the question frequency falls below that — meaning nobody looks at the document while the code keeps changing — keeping four levels loses money.

The number itself depends on the model’s inputs; the result that actually carries over is this: the layered scheme’s payoff depends on how often it is read, not on how good the document looks.

Summary

  • The layered approach models the same system at the context, container, component, and code level; the levels show not different things but the same thing at a different scale. A container is a separately running, separately deployed unit; a component is a separable part inside a container and is not deployed on its own.
  • The model network’s node counts are 6, 17, 28, 61; edge counts are 5, 30, 46, 79. The upper levels’ edges are not written; they’re promoted by replacing the code edges’ ends with their ancestors — 46 of the 46 hand-written component edges come back this way.
  • When the fourteen questions are each read at their own answer level, 62 nodes are read in total; if all of them were answered by looking only at the code level, 278 nodes would be read, and the 216-node difference is unnecessary.
  • Looking too deep gives an expensive answer (61 nodes instead of 6 for D01); looking too high gives none at all: D03 is unanswered at the context level, D11 at all of the first three levels.
  • Four levels keep 112 nodes in maintenance, the code level alone keeps 61; if reading and maintenance are counted as equal cost (VM10), the extra maintenance pays for itself once the question set is asked every 4.24 change rounds.

Next Step

Both lessons measured a single structure made of nodes and edges: what depends on what. Every question asked was of that kind. But not all of someone’s later questions take the form “what depends on what.” Questions like “what order do the calls go in when a loan request comes in,” “what states does a copy pass through,” “which machine does this unit run on” cannot be answered by looking at a node-edge structure; a notation that does not carry order, state, and placement does not contain the answer to these questions. The next lesson separates diagram types by their function — structure-showing, sequence-showing, state-showing, deployment-showing — and measures: which type is the right one per question, what information a question is left missing when answered with the wrong type, and how many items each type has to update per code change.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close