Lesson 01 / 11
Software Architecture Definition
Defining software architecture as a set of decisions that are expensive to reverse: reading each decision's cost of reversal from the dependent closure in a model codebase, ranking decisions by that number, writing down the source of the threshold, and comparing that ranking against one by body size.
Contents
The prerequisite curricula established decisions themselves. The Software Design and Architectural Principles curriculum defined coupling, cohesion, design patterns, and architectural styles, and measured each of them; the System Design and Distributed Systems curriculum tied scaling, resilience, and data distribution decisions from assumption to calculation, and from calculation to measurement. Both curricula answered the same question: what is the difference between these options, and what number does it show up as.
Neither asked: who makes this decision, on what basis, by ruling out which alternative, and what happens to it after it is made. This curriculum’s subject is not decisions themselves but the process and the trail a decision leaves. The first question is a definition: which of the hundreds of decisions in a codebase count as “architectural”? This lesson ties that distinction to a number — the decision’s cost of reversal.
The Decision’s Body and Its Reversal
A decision sits somewhere in the code. The body of the decision “the loan period rule is read from configuration” is the rule module; the body of the decision “authentication is consolidated in a single module” is that module. The body can be small. The cost of reversing the decision, though, is measured not by the body but by everything that depends on the body: breaking the decision takes more than changing the body — it requires touching every module directly or indirectly dependent on it.
Measurement needs a codebase. The file below is the model for the example that will run throughout this curriculum: the software system of a regional library network. The network has multiple branches, an IT unit with its own budget, an externally sourced catalog system, and in-house loan services. This is a model; it does not describe a real institution. The module names, file counts, and import edges are chosen by hand (WA1).
// architecture/codebase.mjs — MODEL codebase for the regional library network software system. // Not a real institution; module names, file counts, and import edges are chosen by hand. export const FILES = { configuration: 3, logging: 4, clock: 2, "data-access": 9, "event-bus": 7, authentication: 8, authorization: 5, "catalog-connector": 11, "catalog-cache": 6, "loan-rule": 5, "loan-core": 14, reservation: 8, "member-registry": 9, "member-penalty": 6, "branch-inventory": 10, "branch-sync": 12, "notification-queue": 6, "notification-email": 15, "report-daily": 7, "report-batch": 13, "web-front": 18, "staff-front": 22, "kiosk-front": 9, "batch-job": 8, }; // IMPORTS[a] = modules that a imports (a -> b: a depends on b) 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"], }; // DECISIONS[i].body = the modules that are the decision's counterpart in code (the decision's body) 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(FILES); // modules directly or indirectly dependent on b (the reverse closure) export function dependentClosure(core) { const closed = new Set(core); let grew = true; while (grew) { grew = false; for (const m of MODULES) { if (closed.has(m)) continue; if (IMPORTS[m].some((h) => closed.has(h))) { closed.add(m); grew = true; } } } return closed; } export const sumFiles = (set) => [...set].reduce((t, m) => t + FILES[m], 0);
The measure’s definition lives in the dependentClosure function: it starts from a set of modules,
and every module dependent on a module already in the set is added to it; this continues until the
set stops growing. This is the decision’s reach. The cost of reversal is the number of modules
and files in that reach (WA2). The number is an upper bound: it assumes every dependent module
will actually be touched, though some of them may not need a change. Measuring an upper bound
rather than a lower bound is deliberate in this lesson — the question being asked is how expensive
a decision could turn out to be.
Measurement
The second file ranks the eleven decisions and tries two thresholds.
// architecture/reversal.mjs — each decision's cost of reversal and a comparison of two rankings import { FILES, DECISIONS, MODULES, dependentClosure, sumFiles } from "./codebase.mjs"; const totalFiles = sumFiles(new Set(MODULES)); console.log(`model codebase: ${MODULES.length} modules, ${totalFiles} files`); const measurements = DECISIONS.map((k) => { const localFiles = k.body.reduce((t, m) => t + FILES[m], 0); const closed = dependentClosure(k.body); return { name: k.name, localModules: k.body.length, localFiles, spreadModules: closed.size, spreadFiles: sumFiles(closed) }; }); const sorted = [...measurements].sort((a, b) => b.spreadFiles - a.spreadFiles); const col = (s, n) => String(s).padEnd(n); console.log("\n" + col("decision", 60) + col("body", 10) + col("reversal", 12) + "ratio"); console.log("-".repeat(90)); for (const o of sorted) console.log(col(o.name, 60) + col(`${o.localModules}m/${o.localFiles}f`, 10) + col(`${o.spreadModules}m/${o.spreadFiles}f`, 12) + (o.spreadFiles / o.localFiles).toFixed(1) + "x"); // threshold 1: an explicitly chosen rule — more than half the codebase const thresholdA = Math.floor(totalFiles / 2) + 1; const aboveA = sorted.filter((o) => o.spreadFiles >= thresholdA); const avg = (d) => (d.reduce((t, o) => t + o.spreadFiles, 0) / d.length).toFixed(1); console.log(`\nthreshold A (chosen rule, half the codebase) = ${thresholdA} files`); console.log(` above it: ${aboveA.length} decisions (avg. ${avg(aboveA)} files), below it: ${sorted.length - aboveA.length} decisions (avg. ${avg(sorted.slice(aboveA.length))} files)`); console.log(` the decision closest to the threshold is ${thresholdA - sorted[aboveA.length].spreadFiles} files below it`); // threshold 2: the largest absolute gap in the ranking let maxGap = 0, cut = 0; for (let i = 0; i < sorted.length - 1; i++) { const b = sorted[i].spreadFiles - sorted[i + 1].spreadFiles; if (b > maxGap) { maxGap = b; cut = i + 1; } } console.log(`threshold B (largest gap in the ranking) = ${sorted[cut].spreadFiles + 1} files (${sorted[cut - 1].spreadFiles} -> ${sorted[cut].spreadFiles}, a ${maxGap}-file jump)`); console.log(` above it: ${cut} decisions, below it: ${sorted.length - cut} decisions`); console.log(`decisions both thresholds select = ${cut} / ${aboveA.length}; same ranking, different cutoff`); // comparison: ranking by body size / ranking by cost of reversal const A = [...measurements].sort((a, b) => b.localFiles - a.localFiles).map((o) => o.name); const B = sorted.map((o) => o.name); let inverted = 0; for (let i = 0; i < A.length; i++) for (let j = i + 1; j < A.length; j++) if (B.indexOf(A[i]) > B.indexOf(A[j])) inverted++; const pairs = (A.length * (A.length - 1)) / 2; const shared = A.slice(0, 4).filter((x) => B.slice(0, 4).includes(x)).length; console.log(`\nbody ranking <-> reversal ranking: ${inverted}/${pairs} inverted pairs (${((inverted / pairs) * 100).toFixed(0)}%)`); console.log(`overlap in the top four decisions = ${shared}/4`); const topA = measurements.find((o) => o.name === A[0]), topB = measurements.find((o) => o.name === B[0]); console.log(`decision with the largest body : ${topA.localFiles} files body, ${topA.spreadFiles} files reversal`); console.log(`most expensive to reverse : ${topB.localFiles} files body, ${topB.spreadFiles} files reversal`);
model codebase: 24 modules, 217 files decision body reversal ratio ------------------------------------------------------------------------------------------ settings are read from a single source 1m/3f 23m/215f 71.7x data access goes through a single layer 1m/9f 17m/179f 19.9x inventory is synced over the event bus 2m/19f 12m/142f 7.5x the clock source is taken from a single module 1m/2f 12m/118f 59.0x the external catalog is accessed through a single connector 1m/11f 9m/108f 9.8x the loan period rule is read from configuration 1m/5f 7m/82f 16.4x authentication is consolidated in a single module 1m/8f 5m/62f 7.8x the daily report is produced in a separate module 1m/7f 2m/29f 4.1x penalty calculation stays separate from the member record 1m/6f 2m/28f 4.7x email notifications go out with a plain-text body 1m/15f 2m/23f 1.5x the kiosk front is a separate deployment unit 1m/9f 1m/9f 1.0x threshold A (chosen rule, half the codebase) = 109 files above it: 4 decisions (avg. 163.5 files), below it: 7 decisions (avg. 48.7 files) the decision closest to the threshold is 1 files below it threshold B (largest gap in the ranking) = 143 files (179 -> 142, a 37-file jump) above it: 2 decisions, below it: 9 decisions decisions both thresholds select = 2 / 4; same ranking, different cutoff body ranking <-> reversal ranking: 31/55 inverted pairs (56%) overlap in the top four decisions = 2/4 decision with the largest body : 19 files body, 142 files reversal most expensive to reverse : 3 files body, 215 files reversal
The table’s last column is the measure’s real finding. The decision that settings are read from a single source sits in a three-file body; reversing it touches 23 modules and 215 files — nearly the entire model codebase — 71.7 times. The clock source being taken from a single module spreads from a two-file body to 118 files. At the other end, the decision that the kiosk front is a separate deployment unit starts at nine files and ends at nine files: no module depends on it, a ratio of 1.0.
The Threshold’s Source
Ranking alone does not give a distinction; a distinction needs a threshold, and the threshold must have a source. The output tried two thresholds. Threshold A is an explicitly chosen rule: a decision whose reversal touches more than half the codebase (WA3). This rule gives a 109-file cutoff and separates four decisions; the top four average 163.5 files, the bottom seven average 48.7 files — a ratio of 3.4 between them. Threshold B comes from the measured distribution: the largest jump in the ranking is the 37-file gap from 179 down to 142, and it separates two decisions.
The two thresholds give different numbers, and this is the definition’s fragile spot. The decision closest to threshold A stays one file below the boundary; a one-file difference switches a decision’s class. The rule that follows is this: the ranking is defensible, the boundary is open to argument. What should be said when calling a decision “architecturally significant” is not a class but the rank and the number that gives that rank. The sentence “this decision is architectural” is unmeasured; the sentence “reversing this decision touches 23 modules, 215 files” is verifiable.
Why Looking at the Body Is Misleading
The comparison runs two rankings on the same input. The first ranks the decision by the size of its body: how much room the change takes up. The second ranks it by cost of reversal. For the same eleven decisions, the two rankings disagree on 31 of 55 pairs — 56%. Only two decisions are shared in the top four.
The extremes show the difference plainly. The decision with the largest body has a 19-file body and its reversal touches 142 files; the most expensive decision to reverse sits in a three-file body and touches 215 files. The decision that email notifications go out with a plain-text body ranks near the top in body size with 15 files, and third from the bottom in cost of reversal with 23 files. In a review, that is the one that would get flagged as a “large change”; the one that would prove impossible to reverse is the three-line settings-read decision.
This sets the measure’s direction. A decision’s weight is read not from the effort spent writing it, but from how much others have come to assume it. This looks in the same direction as the Software Design and Architectural Principles curriculum’s coupling measure; the difference is that the measure there counts the bond between modules, while this one counts a decision’s reach.
Definition
The measure yields the definition. Software architecture is the set of a system’s decisions that are expensive to reverse. The word “expensive” was tied to a number in this lesson: the number of modules and files that must be touched to break the decision. An architecturally significant decision is one whose number stays above a chosen threshold; the threshold is written down and its source is stated.
The same measure also yields a second distinction. A reversible decision is one whose reach is small — a choice like the kiosk front being a separate deployment unit, which starts and ends at nine files. What gets called an irreversible decision is not an absolute barrier but a decision whose cost of reversal approaches the entire codebase as its reach grows. The difference between the two extremes in this model is 215 to 9, a ratio of 23.9.
The definition’s practical consequence is this: whether a decision is architectural cannot be told by looking at its subject matter. Where the loan period rule is read from looks like a small detail, and it touches 82 files in the model codebase; choosing an architectural style looks like a large decision, and that choice’s real weight is likewise read from its reach. The layered, hexagonal, event-driven, and microservices styles established in the Architectural Styles course will not be retold in this curriculum; this course’s subject is not the style itself but the decision that chooses it.
Summary
- A decision’s body is the set of modules that are its counterpart in code; its cost of reversal is the file count of every module directly or indirectly dependent on that body.
- Eleven decisions were measured in the model codebase (24 modules, 217 files); the most expensive decision had a three-file body and a 215-file reach — 71.7 times.
- There is no distinction without a written threshold: the half-the-codebase rule separated four decisions (averages of 163.5 and 48.7 files), the largest-gap rule separated two decisions; the decision closest to the threshold stayed one file below the boundary.
- Ranking by body size against ranking by cost of reversal disagreed on 31 of 55 pairs; the decision with the largest body touches 142 files, one of the smallest bodies touches 215 files.
- Software architecture is the set of decisions that are expensive to reverse; the sentence “this decision is architectural” is unmeasured — the number of modules and files touched is written instead.
Next Step
This lesson measured decisions on a single codebase and ranked them all on the same plane. Yet the decisions in the output are not all made in the same place. Where settings are read from can stay inside a single application; how inventory is synced across branches concerns more than one application; and accessing the externally sourced catalog system through a single connector binds every branch and the IT unit’s budget. The next lesson distributes the same set of decisions across three levels — application, solution, and enterprise architecture — and counts how many decisions each level covers and how many units it affects. What it actually measures is this: when a decision is made at the wrong level, how many units are left uninformed, and how many times is the same decision made over again.
To keep your progress and take notes, Log in
My notes
Log in to take notes.