Lesson 06 / 11
Architectural Views
Applying a question set to the logical, process, development, and physical views: how many questions each view answers, the class of question no view answers, the repetition where two views answer at once, and how many questions are missed by settling for a single view.
Contents
The previous topic turned decisions into objects: each decision’s context, the alternative it ruled out, the risk it carries, and the debt it leaves behind are all written down. The regional library network’s record stack can now say “the external catalog is reached through a single connector” or “notifications are written to a queue.” But this stack is a record of individual decisions. Someone arriving six months later who reads through all of them learns the list of decisions, not the picture of the system. The answer to “what happens when a loan request comes in” does not sit whole inside any one record; its pieces are scattered across ten separate records.
A view exists to fill that gap. A view is the system seen from a single axis of concern; it carries the information that is meaningful on that axis and nothing else. The common split is four: the logical view, the process view, the development view, and the physical view. This lesson defines all four not by their drawings but by the information items they carry, and measures them with a single question: how many of the questions someone asks later does this view answer.
A View Is a Set of Information
No diagram is drawn. The structure underneath what would be drawn is modeled: a view is a container that can carry certain kinds of information items. The logical view carries modules, responsibilities, dependencies between modules, interfaces, and data entities — but not which machine a module runs on. The process view carries running processes, call order, points of concurrency, and queues — but not the source directory structure. The development view carries the source tree, build units, team ownership, and external dependencies. The physical view carries nodes, network links, placement, and capacity.
The item lists are chosen by hand (VM1); the boundaries might be drawn a little differently on another network. The selection itself is an input to the measurement, not its result.
The question set is also a data structure (VM2). Twenty-four questions, of the kind someone actually asks six months later: where does a rule live, which process handles a request, which directory holds a piece of code, which node runs a service. The information items required for each question’s answer are written down. The rule is a single sentence (VM3): a view answers a question only if it carries every item the answer needs. Carrying half of it is not an answer; the reader has to find the rest somewhere else.
// views/model.mjs — MODEL regional library network: the information items carried by // the four views and the question set someone asks six months later. Not a real // institution; item lists and questions are chosen by hand. // A view carries only the items on its list (VM1). export const VIEW = { logical: ["module", "responsibility", "module-dependency", "interface", "data-entity"], process: ["module", "interface", "process", "call-order", "concurrency", "queue"], development: ["module", "source-tree", "build-unit", "team", "external-dependency"], physical: ["node", "network-link", "placement", "capacity", "build-unit"], }; export const VIEWS = Object.keys(VIEW); // QUESTION — needs: the information items required for an answer (VM2). A view answers // a question only if it carries every needed item (VM3). export const QUESTION = [ { id: "S01", text: "which module holds the loan-period rule", needs: ["module", "responsibility"] }, { id: "S02", text: "does the penalty calculation depend on the member registry", needs: ["module", "module-dependency"] }, { id: "S03", text: "what interface does the catalog connector expose", needs: ["module", "interface"] }, { id: "S04", text: "what is the relationship between a member and a loan record", needs: ["data-entity"] }, { id: "S05", text: "what named parts exist in the system", needs: ["module"] }, { id: "S06", text: "which process handles a reservation request", needs: ["module", "process"] }, { id: "S07", text: "which process consumes the notification queue", needs: ["queue", "process"] }, { id: "S08", text: "what gets locked if two branches check out the same copy at once", needs: ["concurrency"] }, { id: "S09", text: "what order do calls go in during the checkout flow", needs: ["call-order"] }, { id: "S10", text: "is the call from the kiosk front asynchronous", needs: ["call-order", "queue"] }, { id: "S11", text: "which directory holds the penalty calculation code", needs: ["source-tree"] }, { id: "S12", text: "which team owns this module", needs: ["module", "team"] }, { id: "S13", text: "how many separate build units are there", needs: ["build-unit"] }, { id: "S14", text: "which external library does the catalog connector use", needs: ["module", "external-dependency"] }, { id: "S15", text: "which node does the loan service run on", needs: ["placement", "node"] }, { id: "S16", text: "what kind of link connects the branch front to the center", needs: ["network-link", "node"] }, { id: "S17", text: "what is the central node's capacity", needs: ["node", "capacity"] }, { id: "S18", text: "which build unit is placed on which node", needs: ["build-unit", "placement"] }, { id: "S19", text: "which node's release does the team owning the penalty calculation affect", needs: ["team", "placement"] }, { id: "S20", text: "which call in the checkout flow goes over the network", needs: ["call-order", "network-link"] }, { id: "S21", text: "which data entity resides on which node", needs: ["data-entity", "placement"] }, { id: "S22", text: "what is the responsibility of the module holding the lock", needs: ["concurrency", "responsibility"] }, { id: "S23", text: "how many processes does releasing a build unit restart", needs: ["build-unit", "process"] }, { id: "S24", text: "whose job is it when the reservation queue backs up", needs: ["queue", "team"] }, ]; export const ITEMS = [...new Set(Object.values(VIEW).flat())].sort(); export const answers = (v, q) => q.needs.every((i) => VIEW[v].includes(i)); export const answeringViews = (q) => VIEWS.filter((v) => answers(v, q));
Measurement
The second file applies the question set to the four views, counts answered and unanswered questions, extracts the repetition, and runs three schemes through the same stream of changes.
// views/measure.mjs — the question set is applied to the four views; answered, // unanswered, repeated questions and maintenance cost are counted. import { VIEW, VIEWS, QUESTION, ITEMS, answeringViews } from "./model.mjs"; const col = (s, n) => String(s).padEnd(n); const abbr = { logical: "log", process: "prc", development: "dev", physical: "phy" }; const measured = QUESTION.map((q) => ({ ...q, a: answeringViews(q) })); console.log(col("question", 10) + col("needed item(s)", 34) + "answering view(s)"); console.log("-".repeat(72)); for (const q of measured) console.log(col(q.id, 10) + col(q.needs.join("+"), 34) + (q.a.length ? q.a.map((v) => abbr[v]).join(",") : "- none -")); const single = measured.filter((q) => q.a.length === 1).length; const multi = measured.filter((q) => q.a.length > 1); const zero = measured.filter((q) => q.a.length === 0); console.log(`\n${QUESTION.length} questions: single view ${single}, multiple views ${multi.length}, none ${zero.length}`); console.log(`the four views together answer ${QUESTION.length - zero.length}/${QUESTION.length} questions`); console.log("\nview answered missed most frequent missing item"); console.log("-".repeat(62)); for (const v of VIEWS) { const missed = measured.filter((q) => !q.a.includes(v)); const counts = {}; for (const q of missed) for (const i of q.needs.filter((x) => !VIEW[v].includes(x))) counts[i] = (counts[i] || 0) + 1; const top = Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3); console.log(col(v, 13) + col(QUESTION.length - missed.length, 10) + col(missed.length, 11) + top.map(([i, n]) => `${i}(${n})`).join(" ")); } // ---- repetition: how many views repeat an item ---- const viewCount = (i) => VIEWS.filter((v) => VIEW[v].includes(i)).length; const totalWrites = ITEMS.reduce((t, i) => t + viewCount(i), 0); console.log(`\n${ITEMS.length} distinct information items are written ${totalWrites} times; ${totalWrites - ITEMS.length} of those writes repeat`); console.log("repeated item: " + ITEMS.filter((i) => viewCount(i) > 1).map((i) => `${i} x${viewCount(i)}`).join(", ")); console.log("repeated question: " + multi.map((q) => `${q.id}(${q.a.length})`).join(", ")); // ---- cross-mappings the unanswered questions need ---- const pairName = (a, b) => [a, b].sort().join(" <-> "); const mappings = [...new Set(zero.map((q) => pairName(q.needs[0], q.needs[1])))]; console.log(`\n${zero.length} unanswered questions need ${mappings.length} cross-mappings:`); for (const q of zero) { const owners = q.needs.map((i) => VIEWS.filter((v) => VIEW[v].includes(i)).map((v) => abbr[v]).join("/")); console.log(` ${q.id} ${col(pairName(q.needs[0], q.needs[1]), 36)}(${owners.join(" and ")})`); } // ---- maintenance: 200 changes across three schemes ---- // WEIGHT — how often each information item changes (VM4); chosen by hand const WEIGHT = { module: 10, responsibility: 8, "module-dependency": 12, interface: 9, "data-entity": 6, process: 5, "call-order": 7, concurrency: 3, queue: 4, "source-tree": 6, "build-unit": 4, team: 3, "external-dependency": 5, node: 2, "network-link": 2, placement: 3, capacity: 2, }; function prng(seed) { let s = seed >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32; } const rand = prng(70321); // seed is visible; the same seed gives the same sequence const totalWeight = ITEMS.reduce((t, i) => t + WEIGHT[i], 0); const pickItem = () => { let r = rand() * totalWeight; for (const i of ITEMS) if ((r -= WEIGHT[i]) < 0) return i; return ITEMS.at(-1); }; const N = 200; let singleWrites = 0, fourWrites = 0, fourRepeats = 0, mappingRefresh = 0; for (let i = 0; i < N; i++) { const item = pickItem(); if (VIEW.process.includes(item)) singleWrites++; const k = viewCount(item); fourWrites += k; fourRepeats += k - 1; mappingRefresh += mappings.filter((m) => m.split(" <-> ").includes(item)).length; } const bestSingle = VIEWS.map((v) => [v, measured.filter((q) => q.a.includes(v)).length]).sort((a, b) => b[1] - a[1])[0]; console.log(`\n${N} change events, best single view = ${bestSingle[0]}`); console.log(col("scheme", 26) + col("answers", 8) + col("writes", 8) + "writes per answer"); console.log("-".repeat(64)); const row = (name, ans, writes) => console.log(col(name, 26) + col(`${ans}/${QUESTION.length}`, 8) + col(writes, 8) + (writes / ans).toFixed(2)); row("single view", bestSingle[1], singleWrites); row("four views", QUESTION.length - zero.length, fourWrites); row("four views + mapping", QUESTION.length, fourWrites + mappingRefresh); console.log(`of the four views' ${fourWrites} writes, ${fourRepeats} write the same fact a second time`); console.log(`${mappings.length} mappings refresh ${mappingRefresh} times across ${N} changes, opening up ${zero.length} questions in return`);
question needed item(s) answering view(s) ------------------------------------------------------------------------ S01 module+responsibility log S02 module+module-dependency log S03 module+interface log,prc S04 data-entity log S05 module log,prc,dev S06 module+process prc S07 queue+process prc S08 concurrency prc S09 call-order prc S10 call-order+queue prc S11 source-tree dev S12 module+team dev S13 build-unit dev,phy S14 module+external-dependency dev S15 placement+node phy S16 network-link+node phy S17 node+capacity phy S18 build-unit+placement phy S19 team+placement - none - S20 call-order+network-link - none - S21 data-entity+placement - none - S22 concurrency+responsibility - none - S23 build-unit+process - none - S24 queue+team - none - 24 questions: single view 15, multiple views 3, none 6 the four views together answer 18/24 questions view answered missed most frequent missing item -------------------------------------------------------------- logical 5 19 placement(4) build-unit(3) call-order(3) process 7 17 placement(4) build-unit(3) node(3) development 5 19 placement(4) call-order(3) node(3) physical 5 19 module(7) call-order(3) process(3) 17 distinct information items are written 21 times; 4 of those writes repeat repeated item: build-unit x2, interface x2, module x3 repeated question: S03(2), S05(3), S13(2) 6 unanswered questions need 6 cross-mappings: S19 placement <-> team (dev and phy) S20 call-order <-> network-link (prc and phy) S21 data-entity <-> placement (log and phy) S22 concurrency <-> responsibility (prc and log) S23 build-unit <-> process (dev/phy and prc) S24 queue <-> team (prc and dev) 200 change events, best single view = process scheme answers writes writes per answer ---------------------------------------------------------------- single view 7/24 85 12.14 four views 18/24 280 15.56 four views + mapping 24/24 394 16.42 of the four views' 280 writes, 80 write the same fact a second time 6 mappings refresh 114 times across 200 changes, opening up 6 questions in return
Answered and Unanswered
The four views together answer eighteen of the twenty-four questions. Fifteen have their answer in a single view, three have their answer in more than one view. Six questions find no answer in any view.
The unanswerable six share a common trait, and this trait is the lesson’s real finding: each one asks for two items to be combined that live in two different views. The question of which node’s release the team owning the penalty calculation affects cannot be answered without setting the development view’s team information next to the physical view’s placement information. Which call in the loan flow goes over the network asks for the process view’s call order to be mapped onto the physical view’s network link. How many processes releasing a build unit restarts is a mapping between the development and process views.
This does not come from the four views being poorly chosen. Because views are split by definition along an axis of concern, the class of question that falls between axes stays open. No matter how well the four views are written, these six questions will not be sitting there; they need something separate — a mapping between views. The six separate mappings the six questions call for are listed at the end of the output.
Counting the unanswered question is this course’s most valuable measure, because someone reading the document who cannot find an answer to their question does not report it to anyone; they look inside the code, ask someone, or guess. If the count is not kept, the gap stays invisible.
What a Single View Misses
The table below shows what each view does on its own. The process view does best: it answers seven of the twenty-four questions and misses seventeen. The logical, development, and physical views each answer five questions and miss nineteen.
The “most frequent missing item” column names the class of the missed questions. Used alone, the logical view most often needs placement (4 questions), call order (3), and node (3) information — that is, what it misses is the runtime and placement class. The physical view’s gap is entirely different: module information is missing in seven questions. The physical view knows where something runs; it does not name what runs.
The practical conclusion here is this: if a system’s document consists of a single view, the questions it misses do not scatter at random; they’re always from the same class. A document that settles for the logical view systematically leaves placement and runtime questions unanswered; a document that settles for the physical view leaves the question “what runs on this node” unanswered.
The Cost of Repetition
Three questions are answered in more than one view. At first glance this looks good — information findable from two places. The cost sits on the item side: seventeen distinct information items are written into views twenty-one times. Four writes are repeats. The module identity repeats in three views, the interface in two views, the build unit in two views. When a module’s name changes, it has to change in three places; if one of the three is forgotten, two views contradict each other.
The table below counts this across a stream of changes. How often items change comes from a hand-picked weight table (VM4); a generator seeded with 70321 produces two hundred change events. The same seed gives the same sequence.
The four-view scheme asks for 280 writes against two hundred changes; 80 of those write the same fact a second or third time. In return, eighteen questions get answered: 15.56 writes per answer. The single-view scheme settles for 85 writes but answers only seven questions: 12.14 writes per answer. Once six cross-mappings are added on top of the four, the whole set gets answered; the mappings refresh 114 times across two hundred changes and the writes per answer climb to 16.42.
The way to read the ranking is this: as scope grows, cost per answer grows too, but the growth is slow. While the number of answers climbs from seven to twenty-four — more than triple — the writes per answer climb from 12.14 to 16.42, about a third more. The cheapness of a single view is an illusion: what’s cheap is not the writing, it is that the seventeen unwritten questions have been handed off to the reader. These numbers depend on the model’s inputs; if the weight table gave placement and node items a higher value, the physical view’s share would grow, but the ranking would not change.
Summary
- A view is the system seen from a single axis of concern and is defined not by its drawing but by the set of information items it carries; the item lists of the logical, process, development, and physical views are distinct and overlap in only three items.
- Across the twenty-four-question set, the four views together answer 18 questions; 15 questions find their answer in a single view, 3 in more than one view, 6 in none.
- All six unanswered questions ask for items from two different views to be combined; this gap comes not from poorly chosen views but from their being split by axis, and it calls for six cross-mappings.
- Settling for a single view, the best case is 7/24 (the process view); the missed questions are not random but from the same class — the logical view misses placement and runtime questions, the physical view misses module identity in seven questions.
- Seventeen information items are written into views 21 times, 4 writes being repeats; with a generator seeded at 70321, across 200 changes the four views ask for 280 writes, 80 of them a second write of the same fact. Writes per answer are 12.14 for a single view, 15.56 for four views, and 16.42 once mappings are added.
Next Step
The four-view scheme cut the system along its axis of concern: what, when, whose code, where. One axis stayed uncut. In this model the logical view carries every module in a single list; someone asking how the network looks from outside and someone asking where a function sits inside a module have to look at the same list. The first reads far more than they were looking for; the second never finds the detail they wanted. The next lesson models the same system at four scale levels from context down to code, and measures: how many nodes and edges per level, at which level a question gets answered, and how many unnecessary nodes someone looking at the wrong level reads.
To keep your progress and take notes, Log in
My notes
Log in to take notes.