Lesson 08 / 11
Decision Making
Making reversible and irreversible decisions with separate methods: computing reversal cost from a dependency set, splitting the same decision set into two classes by that cost, running the quick-trial and long-evaluation methods on both classes, and comparing wait time against wrong-decision cost across four policies.
Contents
The previous lesson computed a simplification’s price but left one question open: can the removed wrapper be brought back? Gathering the config reading scattered across six modules back into a single point is not a job of the same size as removing the wrapper. Some decisions are like this; reversing them costs many times more than making them.
This gap goes unseen most of the time in practice, because every decision is made in the same place at the same speed. The result is a two-way waste: weeks of meetings get held for reversible decisions, and irreversible decisions get closed in a single afternoon. What this lesson measures is how many person-days these two mistakes cost.
Reversal Cost Is Read From a Dependency Set
The degree to which a decision is reversible is not a feeling, it is a computation. A decision lands on a component; reversing it requires traversing that component and every component that depends on it. The model is the library network’s software system; it is fiction, no real institution is described.
// decision/cost.mjs — reversal cost is read from a set of dependencies; this is a fictional model // DEPENDENCY[x] = the components x uses. Reversal requires traversing the changed component and // every component that depends on it (AP7). export const DEPENDENCY = { "data-store": [], "loan-core": ["data-store"], "membership": ["data-store"], "catalog-gateway": ["data-store"], "reservation": ["loan-core"], "reporting": ["data-store", "loan-core"], "payment-gateway": ["loan-core"], "notification": ["reservation"], "matching-pipeline": ["catalog-gateway", "data-store"], "branch-client": ["loan-core", "reservation", "catalog-gateway"], }; // AP7 coefficients (person-days). Traversal per component 2; if the written records' format // changes, migration is 15; if an exposed interface is versioned, coordination is 20. export const COEFFICIENT = { component: 2, migration: 15, contract: 20 }; export const DECISION = [ { name: "loan-record-format", component: "data-store", formatChange: true, external: false }, { name: "branch-client-cache", component: "branch-client", formatChange: false, external: false }, { name: "catalog-gateway-version", component: "catalog-gateway", formatChange: false, external: true }, { name: "notification-text-template", component: "notification", formatChange: false, external: false }, { name: "reporting-query-plan", component: "reporting", formatChange: false, external: false }, { name: "loan-rule-engine", component: "loan-core", formatChange: false, external: false }, { name: "reservation-ordering-rule", component: "reservation", formatChange: false, external: false }, { name: "payment-interface-version", component: "payment-gateway", formatChange: false, external: true }, ]; export function reached(component) { const set = new Set([component]); let grew = true; while (grew) { grew = false; for (const [x, uses] of Object.entries(DEPENDENCY)) if (!set.has(x) && uses.some((y) => set.has(y))) { set.add(x); grew = true; } } return set; } export const reversalCost = (k) => reached(k.component).size * COEFFICIENT.component + (k.formatChange ? COEFFICIENT.migration : 0) + (k.external ? COEFFICIENT.contract : 0);
The computation has three components, and each counts a different fact. The count of reached components is read from the graph and depends on where the decision lands. If the record format changed, reversal concerns not only the code but the written records too; this share stands there even without touching code. If an exposed interface has been versioned, reversal cannot be done unilaterally, it requires coordination with the other side.
Two Classes
// decision/classify.mjs — reversal cost is computed, decisions are split into two classes import { DECISION, reached, reversalCost } from "./cost.mjs"; // AP8: the threshold is the capacity set aside for reversal in one release cycle: 10 person-days. const THRESHOLD = 10; console.log("decision | component | reached | migr | ext | reversal cost"); console.log("----------------------------|------------------|-----------|------|------|--------------"); for (const k of DECISION) console.log(`${k.name.padEnd(27)} | ${k.component.padEnd(16)} | ` + `${String(reached(k.component).size).padStart(9)} | ${(k.formatChange ? "yes" : "-").padStart(4)} | ` + `${(k.external ? "yes" : "-").padStart(4)} | ${String(reversalCost(k)).padStart(13)}`); const classOf = (k) => (reversalCost(k) <= THRESHOLD ? "reversible" : "irreversible"); const group = { reversible: [], irreversible: [] }; for (const k of DECISION) group[classOf(k)].push(k); console.log(`\nthreshold ${THRESHOLD} person-days (AP8)`); for (const [name, list] of Object.entries(group)) console.log(` ${name.padEnd(15)}: ${list.length} decisions, costs ` + `${list.map(reversalCost).join(", ")} (total ${list.reduce((t, k) => t + reversalCost(k), 0)})`);
decision | component | reached | migr | ext | reversal cost ----------------------------|------------------|-----------|------|------|-------------- loan-record-format | data-store | 10 | yes | - | 35 branch-client-cache | branch-client | 1 | - | - | 2 catalog-gateway-version | catalog-gateway | 3 | - | yes | 26 notification-text-template | notification | 1 | - | - | 2 reporting-query-plan | reporting | 1 | - | - | 2 loan-rule-engine | loan-core | 6 | - | - | 12 reservation-ordering-rule | reservation | 3 | - | - | 6 payment-interface-version | payment-gateway | 1 | - | yes | 22 threshold 10 person-days (AP8) reversible : 4 decisions, costs 2, 2, 2, 6 (total 12) irreversible : 4 decisions, costs 35, 26, 12, 22 (total 95)
Four of the eight decisions are reversible decisions, four are irreversible decisions, and
95 of the total reversal cost — 88.8% — is concentrated in the four decisions. The distribution of
the numbers is also instructive: payment-interface-version lands on just one component, its
reached count is 1, but because it versions an exposed interface, its cost is 22.
loan-rule-engine spreads across six components but its reversal cost is 12. That is, “how
many places it touches” alone does not determine the class; what most often makes a decision
irreversible is not the number of places it touches, but the promise it makes outward or the
record it writes.
Two Methods, Two Classes
The comparison runs two decision methods on the same decision set. Quick trial makes the decision in two days and has a high rate of turning out wrong; long evaluation waits fifteen days and has a low rate of turning out wrong. The two methods produce two separate costs: every day waited writes a delay cost, and every decision that turns out wrong writes its reversal cost.
// decision/method.mjs — two decision methods on two classes: wait time and wrong-decision cost import { DECISION, reversalCost } from "./cost.mjs"; // AP9: quick-trial waits 2 days and 35% of its decisions get reversed; long-evaluation // waits 15 days and 10% get reversed. Each day waiting writes 0.4 person-days of delay cost. const METHOD = { "quick-trial": { wait: 2, wrong: 0.35 }, "long-evaluation": { wait: 15, wrong: 0.10 }, }; const DAILY = 0.4, THRESHOLD = 10; const expected = (m, totalReversal, count) => m.wait * count * DAILY + m.wrong * totalReversal; const group = { reversible: [], irreversible: [] }; for (const k of DECISION) group[reversalCost(k) <= THRESHOLD ? "reversible" : "irreversible"].push(k); console.log("class | decisions | reversal cost | quick-trial | long-evaluation | winner"); console.log("----------------|-----------|---------------|--------------|------------------|-------"); for (const [name, list] of Object.entries(group)) { const t = list.reduce((s, k) => s + reversalCost(k), 0); const q = expected(METHOD["quick-trial"], t, list.length); const l = expected(METHOD["long-evaluation"], t, list.length); console.log(`${name.padEnd(15)} | ${String(list.length).padStart(9)} | ${String(t).padStart(13)} | ` + `${q.toFixed(2).padStart(12)} | ${l.toFixed(2).padStart(16)} | ${q < l ? "quick" : "long"} (diff ${Math.abs(q - l).toFixed(2)})`); } const quick = METHOD["quick-trial"], long = METHOD["long-evaluation"]; const breakeven = (long.wait - quick.wait) * DAILY / (quick.wrong - long.wrong); console.log(`\nmethod breakeven point: once reversal cost passes ${breakeven.toFixed(1)} person-days` + ` long-evaluation gets cheaper`); console.log(`class threshold (AP8) ${THRESHOLD}, breakeven point ${breakeven.toFixed(1)}: the two numbers are not the same`); const POLICY = { "always-quick": () => "quick-trial", "always-long": () => "long-evaluation", "by-threshold": (k) => (reversalCost(k) <= THRESHOLD ? "quick-trial" : "long-evaluation"), "by-breakeven": (k) => (reversalCost(k) <= breakeven ? "quick-trial" : "long-evaluation"), }; console.log("\npolicy | wait (days) | expected cost (person-days)"); console.log("----------------|---------------|----------------------------"); for (const [name, choose] of Object.entries(POLICY)) { let days = 0, cost = 0; for (const k of DECISION) { const m = METHOD[choose(k)]; days += m.wait; cost += m.wait * DAILY + m.wrong * reversalCost(k); } console.log(`${name.padEnd(15)} | ${String(days).padStart(13)} | ${cost.toFixed(2).padStart(27)}`); }
class | decisions | reversal cost | quick-trial | long-evaluation | winner ----------------|-----------|---------------|--------------|------------------|------- reversible | 4 | 12 | 7.40 | 25.20 | quick (diff 17.80) irreversible | 4 | 95 | 36.45 | 33.50 | long (diff 2.95) method breakeven point: once reversal cost passes 20.8 person-days long-evaluation gets cheaper class threshold (AP8) 10, breakeven point 20.8: the two numbers are not the same policy | wait (days) | expected cost (person-days) ----------------|---------------|---------------------------- always-quick | 16 | 43.85 always-long | 120 | 58.70 by-threshold | 68 | 40.90 by-breakeven | 55 | 38.70
The first table is the lesson’s core. The same two methods, with the same measure, give opposite results across the two classes. In the reversible class, quick trial is 17.80 person-days cheaper; in the irreversible class, long evaluation is 2.95 person-days cheaper. Neither method is better than the other; a method mismatched to its class is bad.
The magnitude of the two gaps is not equal either. The wrong method’s penalty is 17.80 for reversible decisions and 2.95 for irreversible ones. That is, in this model, the cost of doing long evaluation for a reversible decision is larger than the cost of quick trial for an irreversible decision — because long evaluation’s delay cost is paid for every decision, while reversal cost is paid only if the decision turns out wrong.
The Threshold’s Source, Again
The lower table runs four policies on the same eight decisions. The always-long policy is both
the most expensive and the slowest: 58.70 person-days and 120 days of waiting. always-quick is
the fastest, at 16 days, but its cost is higher than the policies that split by class. Splitting
the two classes shortens the wait time by 52 days relative to always-long, while also dropping
the cost by 17.80 person-days.
But there is a mismatch between the two thresholds, and this is where the threshold’s source
measure from the first lesson shows up here. The class threshold was taken as 10 person-days; its
source was the capacity set aside for reversal in one release cycle. The method’s breakeven point,
however, is 20.8 person-days: this number was not chosen, it was derived from the methods’ wait
time and wrong rate. Because the two are not the same, the by-threshold policy sends the
loan-rule-engine decision (reversal cost 12) to long evaluation, even though that decision is
below the breakeven point. The by-breakeven policy sends the same decision to quick trial and
gains both 13 days and 2.20 person-days.
The rule that comes out of this is narrower and more useful than the classification itself: the class threshold is derived from the method, not chosen. The moment the two decision methods’ wait time and wrong rate are written down, the threshold becomes computable. When a number coming from somewhere else, like capacity, is used as the threshold, classification still gains something, but it stays below what it could gain.
Summary
- A decision’s reversal cost is read from a dependency set, not from a feeling: the count of reached components, the migration of written records, and the coordination of an exposed interface.
- In the eight-decision set, 88.8% of the total reversal cost is concentrated in four irreversible decisions; the count of reached components alone does not determine the class.
- The same two methods gave opposite results across the two classes: quick trial is 17.80 person-days cheaper in the reversible class, and long evaluation is 2.95 person-days cheaper in the irreversible class.
- When four policies are run on the same set,
always-longcame out both the most expensive and the slowest (58.70 person-days, 120 days); the policy that splits by class dropped to 40.90 person-days and 68 days. - The class threshold was 10, the method’s breakeven point was 20.8; the policy that derives the threshold from the breakeven point came out best of the four, at 38.70 person-days and 55 days.
- The threshold is not chosen, it is computed from the two methods’ wait time and wrong rate.
Next Step
Everything measured up to this point ends at the architect’s desk: weight, complexity share,
reversal cost, threshold. None of the eight decisions gets applied at that desk. The
loan-record-format decision’s reversal cost was computed as 35 person-days, but what this
number means differs for the unit holding the budget, differs for the team that will do the
record migration, and is something else entirely for the staff member doing the work at the
branch. The computation stays the same; how the other side reads it changes. The next lesson turns
communication with non-technical stakeholders into a data structure: when the same decision is
presented to the same stakeholders with two different narrations, the questions asked and the
rounds needed for approval are counted.
To keep your progress and take notes, Log in
My notes
Log in to take notes.