Lesson 02 / 11
Architecture Levels
Separating application, solution, and enterprise architecture by decision scope: distributing the same set of decisions across three levels by how many units they operate, the number of decisions each level covers and the number of units it affects, and the counts of uninformed units, repeated decisions, and conflicting implementations when a decision is made at the wrong level.
Contents
The previous lesson measured eleven decisions on a single codebase and ranked them all on the same plane. Yet those decisions are not all made in the same place. Where settings are read from binds the entire system; the daily report being produced in a separate module does not reach beyond two units; the kiosk front being a separate deployment unit stays within itself alone.
Architecture levels are the name for this distinction: application architecture, solution architecture, and enterprise architecture. The three levels are usually described by title. In this lesson they are defined not by title but by scope, and the scope is counted: how many deployment units a decision binds.
Scope Determines the Level
The previous lesson’s reach measure was at the module level. The question of level needs one more mapping: which unit runs which module. A deployment unit is a piece that is released separately, runs separately, and is usually looked after by a separate team; it looks in the same direction as the bounded context distinction established in the Software Design and Architectural Principles curriculum’s Domain-Driven Design course, but here what draws the boundary is not the domain but operating responsibility.
A decision’s scope is this: the set of units that run at least one of the modules in the decision’s reach (WA4). If the scope is one unit, the decision is at the application level; if it binds the whole network or most of it, it is enterprise; if it falls between the two, it is at the solution level. “Most of it” is a threshold, and the threshold’s source must be written down: here, two-thirds of the units was chosen (WA5).
The first file gives the previous lesson’s import graph and decision bodies again; the graph is repeated so the block runs on its own.
// architecture/model.mjs — MODEL library network codebase: import graph and decision bodies. // Not a real institution; edges and decision bodies are chosen by hand. export const IMPORTS = { configuration: [], clock: [], logging: ["configuration"], "data-access": ["configuration", "logging"], "event-bus": ["configuration", "logging"], authentication: ["data-access", "configuration", "logging"], authorization: ["authentication"], "catalog-connector": ["configuration", "logging"], "catalog-cache": ["catalog-connector", "clock"], "loan-rule": ["configuration", "clock"], "loan-core": ["loan-rule", "data-access", "event-bus", "clock", "catalog-connector"], reservation: ["loan-core", "notification-queue", "data-access"], "member-registry": ["data-access", "logging"], "member-penalty": ["member-registry", "loan-rule", "clock", "data-access"], "branch-inventory": ["data-access", "event-bus"], "branch-sync": ["branch-inventory", "event-bus", "catalog-connector", "logging"], "notification-queue": ["data-access", "event-bus"], "notification-email": ["notification-queue", "configuration"], "report-daily": ["data-access", "clock"], "report-batch": ["data-access", "branch-inventory", "clock", "logging"], "web-front": ["loan-core", "catalog-cache", "member-registry", "reservation", "authentication"], "staff-front": ["loan-core", "member-registry", "member-penalty", "branch-inventory", "report-daily", "authentication", "authorization"], "kiosk-front": ["loan-core", "catalog-cache", "authentication"], "batch-job": ["report-batch", "branch-sync", "notification-queue", "notification-email"], }; export const DECISIONS = [ { name: "data access goes through a single layer", body: ["data-access"] }, { name: "settings are read from a single source", body: ["configuration"] }, { name: "inventory is synced over the event bus", body: ["event-bus", "branch-sync"] }, { name: "the external catalog is accessed through a single connector", body: ["catalog-connector"] }, { name: "authentication is consolidated in a single module", body: ["authentication"] }, { name: "the loan period rule is read from configuration", body: ["loan-rule"] }, { name: "the clock source is taken from a single module", body: ["clock"] }, { name: "penalty calculation stays separate from the member record", body: ["member-penalty"] }, { name: "the daily report is produced in a separate module", body: ["report-daily"] }, { name: "email notifications go out with a plain-text body", body: ["notification-email"] }, { name: "the kiosk front is a separate deployment unit", body: ["kiosk-front"] }, ]; export const MODULES = Object.keys(IMPORTS); export function dependentClosure(core) { const closed = new Set(core); for (let grew = true; grew; ) { grew = false; for (const m of MODULES) if (!closed.has(m) && IMPORTS[m].some((h) => closed.has(h))) { closed.add(m); grew = true; } } return closed; }
Measurement
The second file builds the unit mapping, counts each decision’s scope, and runs the same set of decisions across three schemes.
// architecture/level.mjs — the number of units a decision covers, and its spread across three levels import { DECISIONS, dependentClosure } from "./model.mjs"; // UNIT — deployment units of the MODEL library network; which modules each unit runs const UNIT = { "web-portal": ["web-front"], "staff-app": ["staff-front"], "kiosk-network": ["kiosk-front"], "loan-service": ["loan-core", "loan-rule", "reservation"], "member-service": ["member-registry", "member-penalty"], "catalog-integration": ["catalog-connector", "catalog-cache"], "inventory-service": ["branch-inventory", "branch-sync"], "notification-service": ["notification-queue", "notification-email"], "report-service": ["report-daily", "report-batch", "batch-job"], "shared-infrastructure": ["configuration", "logging", "clock", "data-access", "event-bus", "authentication", "authorization"], }; const UNITS = Object.keys(UNIT); const U = UNITS.length; const ENTERPRISE_THRESHOLD = Math.ceil((U * 2) / 3); // chosen rule: two-thirds of the units const scope = (k) => { const reach = dependentClosure(k.body); return UNITS.filter((u) => UNIT[u].some((m) => reach.has(m))); }; const levelName = (n, e = ENTERPRISE_THRESHOLD) => (n === 1 ? "application" : n >= e ? "enterprise" : "solution"); const measurements = DECISIONS.map((k) => ({ name: k.name, n: scope(k).length, set: scope(k) })); const col = (s, n) => String(s).padEnd(n); console.log(`${U} units, ${DECISIONS.length} decisions, enterprise threshold = ${ENTERPRISE_THRESHOLD} units\n`); console.log(col("decision", 60) + col("units", 8) + "level"); console.log("-".repeat(78)); for (const o of [...measurements].sort((a, b) => b.n - a.n)) console.log(col(o.name, 60) + col(o.n, 8) + levelName(o.n)); console.log(""); for (const d of ["enterprise", "solution", "application"]) { const g = measurements.filter((o) => levelName(o.n) === d); const affected = new Set(g.flatMap((o) => o.set)); console.log(`${col(d, 13)} decisions=${col(g.length, 4)} units affected=${col(affected.size, 4)} avg. scope=${(g.reduce((t, o) => t + o.n, 0) / g.length).toFixed(1)}`); } console.log("threshold sensitivity: " + [6, 7, 8].map((e) => `threshold ${e} -> ${measurements.filter((o) => levelName(o.n, e) === "enterprise").length} enterprise`).join(", ")); // ---- comparison: same set of decisions across three schemes ---- // OPTIONS[k] = the decision's number of reasonable options; the unit that decides independently picks one of them const OPTIONS = [3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 2]; function prng(seed) { let s = seed >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32; } const rand = prng(48151); // seed is visible; the same seed gives the same numbers let uninformed = 0, repeated = 0, conflicting = 0; measurements.forEach((o, i) => { const others = o.n - 1; // scope units other than the decision's owner uninformed += others; repeated += others; const ownersChoice = Math.floor(rand() * OPTIONS[i]); for (let j = 0; j < others; j++) if (Math.floor(rand() * OPTIONS[i]) !== ownersChoice) conflicting++; }); const consultLocal = 0, consultThreeLevel = measurements.reduce((t, o) => t + o.n - 1, 0), consultCentral = DECISIONS.length * (U - 1); console.log(`\n${col("scheme", 12) + col("uninformed", 12) + col("repeated", 10) + col("conflicting", 13)}consults`); console.log("-".repeat(55)); console.log(col("local", 12) + col(uninformed, 12) + col(repeated, 10) + col(conflicting, 13) + consultLocal); console.log(col("three-level", 12) + col(0, 12) + col(0, 10) + col(0, 13) + consultThreeLevel); console.log(col("centralized", 12) + col(0, 12) + col(0, 10) + col(0, 13) + consultCentral); console.log(`\nthe local scheme avoids ${consultThreeLevel} consults, in exchange it gets ${conflicting} conflicting implementations`); console.log(`break-even: the three-level scheme wins if resolving a conflicting implementation costs more than ${(consultThreeLevel / conflicting).toFixed(2)} consults`); console.log(`the centralized scheme spends ${consultCentral - consultThreeLevel} more consults than the three-level scheme, while conflicts are 0 in both`); const soleScope = measurements.filter((o) => o.n === 1).length; console.log(`of that, ${soleScope * (U - 1)} is spent on the ${soleScope} decision(s) whose scope is a single unit`);
10 units, 11 decisions, enterprise threshold = 7 units decision units level ------------------------------------------------------------------------------ settings are read from a single source 10 enterprise data access goes through a single layer 9 enterprise inventory is synced over the event bus 8 enterprise the clock source is taken from a single module 8 enterprise the external catalog is accessed through a single connector 7 enterprise the loan period rule is read from configuration 5 solution authentication is consolidated in a single module 4 solution penalty calculation stays separate from the member record 2 solution the daily report is produced in a separate module 2 solution email notifications go out with a plain-text body 2 solution the kiosk front is a separate deployment unit 1 application enterprise decisions=5 units affected=10 avg. scope=8.4 solution decisions=5 units affected=8 avg. scope=3.0 application decisions=1 units affected=1 avg. scope=1.0 threshold sensitivity: threshold 6 -> 5 enterprise, threshold 7 -> 5 enterprise, threshold 8 -> 4 enterprise scheme uninformed repeated conflicting consults ------------------------------------------------------- local 47 47 31 0 three-level 0 0 0 47 centralized 0 0 0 99 the local scheme avoids 47 consults, in exchange it gets 31 conflicting implementations break-even: the three-level scheme wins if resolving a conflicting implementation costs more than 1.52 consults the centralized scheme spends 52 more consults than the three-level scheme, while conflicts are 0 in both of that, 9 is spent on the 1 decision(s) whose scope is a single unit
Reading the Distribution
The eleven decisions split into five enterprise, five solution, and one application-level decision. The average scope of the five enterprise-level decisions is 8.4 units, of the five solution-level decisions, 3.0 units. The union column is informative too: the five enterprise-level decisions touch all ten units, the five solution-level decisions touch eight.
The threshold’s fragility is measured here too. Lowering the enterprise threshold from seven to six changes no decision, because no decision has a scope of six units; raising it to eight drops the external catalog connector decision from the enterprise level to the solution level. The threshold is a choice, not a ranking; which decision outranks which comes from the measurement, the boundary comes from a rule.
What the distribution actually says is this: no decision belongs to a level by virtue of its subject matter. Where the clock is read from looks, as a subject, like an application detail; its scope is eight units, and in this model it falls at the same level as inventory sync. A decision’s level comes not from its subject but from how many units depend on it.
The Cost of the Wrong Level
The table below runs the same eleven decisions across three schemes. In the local scheme, each decision is made in the unit where its body sits and stays there. In the centralized scheme, each decision is made by consulting every unit. In the three-level scheme, each decision is made at the level of its measured scope and only the units in that scope are consulted.
In the local scheme, 47 in-scope units never hear the decision. Since a unit that does not hear it still has to get its work done, it makes the same decision on its own: 47 repeated decisions. Not all repeated decisions clash — for each decision the number of reasonable options is between two and four (WA6), and the unit deciding independently picks one of them. With the generator seeded at 48151, 31 of the 47 repeats came out different from the owner’s choice, that is, 31 conflicting implementations.
The local scheme’s gain is that it avoids 47 consults. From here the break-even point is read: if reconciling a conflicting implementation costs more than 1.52 consults, the three-level scheme wins. This is a calculation, not a preference, and the number depends on the model’s inputs; as the option counts grow, conflicting implementations increase and the break-even point drops.
The centralized scheme brings the conflict down to zero too, but with 99 consults — 52 more than the three-level scheme. Nine of those are spent on a single decision: the kiosk front being a separate deployment unit has a scope of one unit, and yet nine more units are consulted anyway. Deciding at the wrong level has two directions, and both have a number: deciding lower than the true level produces uninformed units and conflicting implementations, deciding higher than the true level produces wasted consults.
Summary
- A decision’s level is read from its scope, not its subject; scope is the number of deployment units that run the modules in the decision’s reach.
- The model network has ten units and eleven decisions; when the enterprise threshold is chosen as two-thirds of the units (7 units), five enterprise, five solution, and one application-level decision result.
- The five enterprise-level decisions touch all ten units (average scope 8.4); the five solution-level decisions touch eight units (average scope 3.0).
- In the local scheme, 47 units do not hear the decision, 47 decisions are made again, and with the generator seeded at 48151, 31 of these come out different from the owner’s choice; the break-even point is where a conflicting implementation equals 1.52 consults.
- The centralized scheme brings the conflict to zero but spends 99 consults; nine of those are for the single decision whose scope is one unit.
Next Step
Both lessons treated the decision as something already made: the decision exists, its scope is measured, its level is determined. Who makes the decision, who it is announced to and how once it is made, and whether it is actually applied once announced — none of that was asked. Someone carries out the three-level scheme’s 47 consults; that person’s job is not only to make decisions. The next lesson splits the architect’s job into three responsibilities — decision, communication, and oversight — and builds all three as a matrix. What it measures is this: the share of decisions that, despite having been made and announced, have no counterpart left in the code, and how much that share changes once oversight is added.
To keep your progress and take notes, Log in
My notes
Log in to take notes.