---
title: 'Cloud Fundamentals'
source: 'https://academia.sh/en/courses/cloud-fundamentals'
language: en
updated: '2026-09-13T05:07:33+00:00'
license: 'CC BY-SA 4.0'
lessons: 10
outcomes:
    - 'Separates the service and deployment models by the decision handed over'
    - 'Draws the shared responsibility boundary item by item'
    - 'Compares the pricing and migration options against their cost'
---

# Cloud Fundamentals

What renting is and where its limit sits: tying on-demand resources and elasticity to a number, comparing the infrastructure, platform, and software service models in terms of the decision handed over, the public, private, and hybrid deployment models, the difference in cost structure and responsibility against on-premises, the shared responsibility boundary between provider and customer, the failure-domain separation of regions and availability zones, edge locations, the pillars of the design framework, the price difference between on-demand, reserved, and spot capacity, and migration strategies.

## The Cloud Concept

The definition of renting and its limit: measuring on-demand resources and elasticity, which decision each of the infrastructure, platform, and software service models hands over, the cost and control difference between public, private, and hybrid deployment, comparing the fixed and variable cost structure against an on-premises installation, and drawing the responsibility boundary between provider and customer item by item.

### What Is Cloud Computing

Source: https://academia.sh/en/courses/cloud-fundamentals/what-is-cloud-computing

The measure of on-demand resources: in the same 120-day demand curve, the time from capacity request to use dropping from 30 days to 1 day, unmet requests under the peak-forming load falling from 84,689 to 1,260 and the days capacity ran short dropping from 15 to 2, idle capacity falling from 764 to 254 unit-days, while under a flat load the requests saved stay at zero, and the eight items that remain at exit dropping to four.

The previous curriculum did the same thing across eight courses: it took the machine, the network,
storage, and the cluster one at a time, made a decision in each, and counted that decision's cost.
All of those layers shared one silent assumption — the hardware had **already been purchased**.
This lesson removes that assumption and asks a single question: which number changes if the same
layers are **rented** instead of purchased.

**Cloud computing** is the renting of computing resources **on-demand** instead of purchasing them,
paying only for what is used. What renting is really selling is called **elasticity**: the ability
to raise and lower capacity as demand changes. Elasticity is a layer above M19's **scaling** —
scaling asks how many nodes will run, elasticity asks **how long** it takes to obtain that node.
"The cloud is fast" carries no decision; the measure of speed is a duration, and that duration
produces two costs.

#### Elasticity Is a Number

The example is the course's fictional **regional measurement network**: a service that collects
water-meter readings, a service that validates the readings, billing and work-order services, a
nightly batch job, and five teams. Both the network and the services are fictional.

**CC1:** the model runs a 120-day window; the measured service is the reading collector. **CC2:** one
capacity unit serves 1,200 requests per day. **CC3:** with own hardware, 30 days pass from capacity
request to use, the smallest purchase is 4 units, and acquired capacity is never given back. **CC4:**
under renting the same duration is 1 day, the smallest purchase is 1 unit, and capacity can be
reduced in the same duration. **CC5:** if the highest demand of the last three days exceeds 85% of
capacity, a capacity request is triggered; the target is that peak plus a 20% margin. **CC6:** unmet
requests are the portion by which demand exceeds capacity; idle capacity is the portion by which
capacity exceeds the required unit, and it is summed in unit-days.

The demand curve and the two acquisition methods are a **model**; there is no real measurement
network, real demand record, or real lead time. The generator for the daily wave is written
in-house, and its seed is visible.

```js
// cloud/demand.mjs — the fictional regional measurement network's demand curve and capacity
// acquisition model (model): no real measurement network, demand record, or lead time exists.

export const DAYS = 120;                       // the model runs a 120-day window
export const UNIT = 1200;                      // one capacity unit serves 1,200 requests per day
export const THRESHOLD = 0.85, MARGIN = 1.2;   // once past the threshold, target = peak + 20% margin

// the generator is written in-house, seed is visible: a linear congruential generator, only for the daily wave
export function generator(seed) {
  let s = seed >>> 0;
  return () => ((s = (s * 1103515245 + 12345) >>> 0) % 1000) / 1000;
}

const BASE = {
  flat: () => 5000,
  peak: (d) => 5000 + (d >= 55 && d < 75 ? 13000 * Math.sin(((d - 55) / 20) * Math.PI) : 0),
  growing: (d) => 4000 + 90 * d,
};

export function curve(name) {
  const r = generator(20260401);
  return Array.from({ length: DAYS }, (_, d) => Math.round(BASE[name](d) * (0.94 + 0.12 * r())));
}

// mode: delay = days from capacity request to use, block = smallest purchase unit, shrink = can it be given back
export const MODE = {
  own: { delay: 30, block: 4, shrink: false },
  rent: { delay: 1, block: 1, shrink: true },
};

export function run(curveName, modeName, override = {}) {
  const T = curve(curveName), { delay, block, shrink } = { ...MODE[modeName], ...override };
  const roundUp = (b) => Math.ceil(b / block) * block;
  let cap = roundUp(Math.ceil((Math.max(T[0], T[1], T[2]) * MARGIN) / UNIT));
  const queue = [];                                  // pending capacity requests
  const s = { requests: 0, wait: 0, short: 0, loss: 0, idle: 0, leased: 0, peakCap: cap };

  for (let d = 0; d < DAYS; d++) {
    for (const i of queue.filter((x) => x.arrival === d)) cap = i.target;
    const window = Math.max(...T.slice(Math.max(0, d - 2), d + 1));
    const target = roundUp(Math.ceil((window * MARGIN) / UNIT));
    const pending = queue.filter((x) => x.arrival > d);
    const lastTarget = pending.length ? pending[pending.length - 1].target : cap;
    const grow = window > THRESHOLD * cap * UNIT && target > lastTarget;
    const cut = shrink && target < lastTarget && pending.length === 0;
    if (grow || cut) {
      queue.push({ arrival: d + delay, target });
      if (grow) { s.requests++; s.wait += delay; }
    }
    const served = Math.min(T[d], cap * UNIT);
    if (T[d] > served) { s.short++; s.loss += T[d] - served; }
    s.idle += Math.max(0, cap - T[d] / UNIT);
    s.leased += cap;
    s.peakCap = Math.max(s.peakCap, cap);
  }
  return { ...s, wait: s.requests ? (s.wait / s.requests).toFixed(1) : "-" };
}

// What remains at exit: "+" carries over, "-" embedded in the service, "none" the item does not exist in that form.
export const EXIT = `
  collected measurement data     + +
  service configuration          + +
  provisioning procedure         + +
  operational information        + +
  the hardware itself            + none
  facility contract              + none
  scaling decision               + -
  measured performance history   + -`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s{2,}/);
    const [a, b] = p[1].split(/\s+/);
    return { item: p[0], own: a, rent: b };
  });
```

```js
// cloud/measure.mjs — elasticity is a number: wait, unmet requests, idle capacity, exit.
import { run, curve, EXIT, DAYS, UNIT } from "./demand.mjs";

const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const CURVE = ["flat", "peak", "growing"];

const R = {};
for (const c of CURVE) for (const m of ["own", "rent"]) R[`${c}-${m}`] = run(c, m);
const t = curve("peak");
console.log(`${DAYS}-day window, ${UNIT} requests/day per unit; the peak curve's ` +
  `lowest day is ${Math.min(...t)}, highest day is ${Math.max(...t)} requests`);

const A = [-32, 14, 12];
console.log("\n1. same workload in the peak-forming curve, two acquisition methods");
write(A, "measure", "own hardware", "renting");
for (const [name, f] of [["capacity requests (count)", (r) => r.requests],
  ["request to use (days)", (r) => r.wait],
  ["days capacity ran short", (r) => r.short],
  ["unmet requests", (r) => r.loss],
  ["idle capacity (unit-days)", (r) => r.idle.toFixed(0)],
  ["peak capacity (units)", (r) => r.peakCap]])
  write(A, name, f(R["peak-own"]), f(R["peak-rent"]));

const B = [-10, 14, 13, 13, 12];
console.log("\n2. the same measure across three demand curves (loss = unmet requests)");
write(B, "curve", "own loss", "own idle", "rent loss", "rent idle");
for (const c of CURVE)
  write(B, c, R[`${c}-own`].loss, R[`${c}-own`].idle.toFixed(0),
    R[`${c}-rent`].loss, R[`${c}-rent`].idle.toFixed(0));

const C = [-10, 18, 21, 21];
console.log("\n3. what elasticity bought, and what was leased for it");
write(C, "curve", "requests saved", "leased unit-days", "requests / unit-day");
for (const c of CURVE) {
  const k = R[`${c}-own`].loss - R[`${c}-rent`].loss, bg = R[`${c}-rent`].leased;
  write(C, c, k, bg, (k / bg).toFixed(1));
}

const E = [-24, 10, 12, 16, 16];
console.log("\n4. as lead time shortens (peak curve, 4-unit block, no giving back)");
write(E, "request to use (days)", "requests", "short days", "unmet", "idle unit-days");
for (const lead of [30, 14, 7, 3, 1]) {
  const r = run("peak", "own", { delay: lead });
  write(E, lead, r.requests, r.short, r.loss, r.idle.toFixed(0));
}

const D = [-30, 16, 12];
console.log("\n5. what remains at exit (8 items; none = the item does not exist in that form at all)");
write(D, "item", "own hardware", "renting");
for (const c of EXIT) write(D, c.item, c.own, c.rent);
const count = (a, sym) => EXIT.filter((c) => c[a] === sym).length;
write(D, "PORTABLE / EMBEDDED / NONE", `${count("own", "+")} / ${count("own", "-")} / ` +
  `${count("own", "none")}`, `${count("rent", "+")} / ${count("rent", "-")} / ${count("rent", "none")}`);
```

```
120-day window, 1200 requests/day per unit; the peak curve's lowest day is 4714, highest day is 18619 requests

1. same workload in the peak-forming curve, two acquisition methods
measure                           own hardware     renting
capacity requests (count)                    3          10
request to use (days)                     30.0         1.0
days capacity ran short                     15           2
unmet requests                           84689        1260
idle capacity (unit-days)                  764         254
peak capacity (units)                       20          19

2. the same measure across three demand curves (loss = unmet requests)
curve           own loss     own idle    rent loss   rent idle
flat                   0          459            0         204
peak               84689          764         1260         254
growing            32784          123            0         239

3. what elasticity bought, and what was leased for it
curve         requests saved     leased unit-days  requests / unit-day
flat                       0                  705                  0.0
peak                   83429                  892                 93.5
growing                32784                 1175                 27.9

4. as lead time shortens (peak curve, 4-unit block, no giving back)
request to use (days)     requests  short days           unmet  idle unit-days
30                               3          15           84689             764
14                               3          13           81223             953
7                                3           8           38341            1001
3                                3           4            7647            1024
1                                3           0               0            1037

5. what remains at exit (8 items; none = the item does not exist in that form at all)
item                              own hardware     renting
collected measurement data                   +           +
service configuration                        +           +
provisioning procedure                       +           +
operational information                      +           +
the hardware itself                          +        none
facility contract                            +        none
scaling decision                             +           -
measured performance history                 +           -
PORTABLE / EMBEDDED / NONE           8 / 0 / 0   4 / 2 / 2
```

The numbers are in the **measurement** class; their inputs are CC1–CC6. The first table reduces
elasticity to a single line: **30.0 days versus 1.0 day, request to use.** This one number
determines everything else.

#### The Cost of Waiting and Idle Capacity

The thirty-day wait shows up in two places. In the peak-forming curve, own hardware opens three
capacity requests, but by the time capacity arrives the peak has already passed: capacity **stays
short for 15 days** and **84,689 requests go unmet**. Under renting the same curve produces ten
capacity requests, each one coming into use the next day, and the short days drop from 15 to 2 and
unmet requests drop to 1,260. The cause of the remaining 1,260 requests is not the lead time but
the decision rule: after the threshold is crossed, a one-day wait still applies.

The second cost runs in the opposite direction. Because own hardware cannot be given back, it stays
on hand even after the peak has passed and produces **764 unit-days** of idle capacity; under
renting the same figure is **254 unit-days**, because capacity comes back down. The peak capacities
are nearly identical (20 versus 19); the difference is not in the size of the capacity but in **how
long it is held**.

The growing curve shows how these two costs trade places. There, own hardware's idle capacity is at
its lowest (123 unit-days) — because it is constantly falling behind. That same lag produces 32,784
unmet requests. **Low idle capacity is not good news; it is another name for insufficient
capacity.** Read separately, both costs mislead, but read together, they carry a decision.

#### What Renting Costs You

The third table ties elasticity's payoff to a single ratio: leased unit-days per request saved. In
the peak-forming curve, renting saves 83,429 requests and does it with 892 unit-days of leasing —
93.5 requests per unit-day. In the growing curve the ratio drops to 27.9. **In the flat curve the
ratio is 0.0.**

Under a steady, predictable load, renting gains nothing from elasticity: both forms lose zero
requests. The only difference in the table is in idle capacity (459 versus 204 unit-days), and that
difference comes not from elasticity but from **the smallest purchase unit** — a four-unit block
puts eight units on top of a 4.2-unit need. When the unit shrinks, the gap closes; as long as demand
stays flat, renting's measured payoff is zero and all that is left in hand is decisions that have
been handed over. This does not mean the cloud is "expensive"; it means **something whose measured
payoff is zero does not have a cost of zero**.

#### Is Shortening the Lead Time Enough

The fourth table shortens only the lead time and leaves everything else under own hardware's rule:
a four-unit block, no giving back. Unmet requests are 84,689 at 30 days, 81,223 at 14 days, 38,341
at 7 days, 7,647 at 3 days, and zero at 1 day. Halving the lead time does not halve the loss — the
first step, from 30 to 14 days, erases only 4% of the loss, because the peak lasts twenty days and
a fourteen-day delay still falls inside the peak. **The wait only gains meaning when compared
against the width of the peak.**

The idle capacity next to the same column runs the opposite direction: it climbs from 764 to 1,037
unit-days. Because capacity arrives earlier, it sits on hand longer, and because it cannot be given
back, it stays there. Cutting the lead time to one day zeroes out unmet requests, but drives idle
capacity up to 1,037 unit-days instead of renting's 254. **Elasticity is two separate decisions:
how quickly capacity can be obtained, and whether it can be given back.** Without the second, the
first only moves the cost around.

#### What Remains at Exit

The fifth table counts renting's silent item. Of the eight items, all eight remain in hand under
own hardware. Under renting, four carry over (data, configuration, the provisioning procedure,
operational information), two are embedded in the service (the scaling decision and the measured
performance history sit inside the service), and two items **do not exist at all**: the hardware
and the facility contract.

The last two rows are both renting's gain and its cost. The absence of hardware is the reason the
thirty-day wait disappears; that same absence also means there is no asset left to carry away on
exit. The number of decisions handed over here is three — when to acquire capacity, how much to
acquire, and where the hardware sits — and what is received in exchange is a single number: 30 days
dropping to 1 day. This triple is the smallest form of the calculation that repeats through the
rest of the course: an item is rented, a few decisions leave your hands, a number improves in
exchange, and the item that must carry over at exit shrinks. What is being counted is nowhere "the
cloud"; what is being counted is **which decision sits with whom**.

#### Summary

- Elasticity is a duration: 30.0 days versus 1.0 day, from capacity request to use. In the same
  peak-forming curve this difference drops the short days from 15 to 2 and unmet requests from
  84,689 to 1,260.
- Two costs are counted together: own hardware produces 764 unit-days of idle capacity, renting
  254; the peak capacities, however, are nearly identical (20 versus 19).
- In the growing curve, own hardware's idle capacity is at its lowest (123 unit-days), and that is
  exactly why 32,784 requests go unmet; low idle capacity is another name for insufficient capacity.
- Elasticity's payoff is a ratio: 93.5 (peak), 27.9 (growing), **0.0 (flat)**, per unit-day. Under a
  steady load, the only remaining difference comes from the smallest purchase unit, not from
  elasticity.
- Shortening the lead time alone drops the loss from 84,689 to zero, but idle capacity climbs from
  764 to 1,037 unit-days; without the ability to give capacity back, speed only relocates the cost.
- At exit, all eight items remain in hand under own hardware, but only four under renting; two are
  embedded in the service, two do not exist at all — and that same absence is the reason the wait
  disappears too.

#### Next Step

This lesson rented a single thing: capacity. Three decisions were handed over, and twenty-nine days
were gained in exchange. Yet how much of what is rented gets handed over is not fixed; there is a
large difference between renting only the machine and renting the machine together with the runtime
that sits on top of it. The next lesson opens the same decision set item by item and counts, across
three service models, **how many decisions are handed over, how many stay in hand**, and which of
the handed-over decisions can be **taken back**.

### Service Models

Source: https://academia.sh/en/courses/cloud-fundamentals/service-models

The measure of infrastructure, platform, and software service: in a set of 26 decisions, decisions handed over climbing from 5 to 23 and 25, the yearly touches on kept decisions dropping from 91 to 82, 19, and 4, the share of handovers recoverable via a parameter falling from 40% to 39% and 12%, 6, 5, 4, and 1 of six requirement requests being met, and the carried-over decision share at exit dropping from 100% to 92%, 69%, and 35%.

The previous lesson rented a single thing — capacity — and handed over three decisions. But the
depth of renting is not fixed. Renting only the machine is not the same as renting the machine
together with the runtime on top of it and its scaling behavior; the difference between the two is
**how many decisions leave your hands**. This lesson counts that difference item by item.

The depth of renting is named as three **service models**. **Infrastructure service** rents the
machine, the storage, and the network; everything on top of that stays with the customer.
**Platform service** also rents the runtime; the customer supplies the code and its configuration.
**Software service** rents the work itself; the customer supplies only data and access decisions.
These three definitions carry no decision on their own — what carries a decision is how the same
decision set is split across the three models.

#### The Decision Set

The example is the course's fictional **regional measurement network**: the water-meter reading
collector, the validator, billing and work-order services, a nightly batch job, and five teams.
Both the network and the services are fictional; the decision set and service models below are a
**model** — there is no real service catalog or real settings list.

**CC7:** the decisions that must be made to keep the reading collector service running split into 26
items across ten domains. **CC8:** a decision either stays kept (`E`) in a given model or is handed
over; a handed-over decision's recoverability falls into three classes — recovered via a parameter
(`P`), waits on a contract window (`S`), embedded in the service (`G`). **CC9:** how many times a
year each decision is touched is fixed. **CC10:** a requirement request is met the same day if the
decision is `E`, in 1 day if `P`, in 30 days if `S`; if `G` it can never be met. These durations are
fictional. **CC11:** every kept decision touch produces one operating item. **CC12:** at exit, every
handed-over decision must be given back; those in class `G` additionally require a new solution in
its place.

```js
// cloud/decision.mjs — decision set and three service models for the fictional measurement
// network (model): no real service catalog, real settings list, or real contract exists. No randomness.

export const MODEL = ["own", "infrastructure", "platform", "software"];
// cell: E kept, P recovered via a parameter, S waits on a contract window, G embedded in the service
export const WINDOW = { E: 0, P: 1, S: 30 };   // time to fulfill the request (fictional days)

export const DECISION = `
  hardware          machine-model        E P G G  2
  hardware          disk-type            E P G G  2
  hardware          failure-replacement  E G G G  3
  facility          physical-siting      E S S G  1
  facility          power-and-cooling    E G G G  1
  operating-system  distro-choice        E E G G  1
  operating-system  version              E E G G  2
  patching          security-patch       E E G G 12
  patching          restart              E E S G  6
  runtime           language-version     E E P G  3
  runtime           library-version      E E E G  9
  runtime           build-option         E E S G  2
  scaling           threshold            E E P G  4
  scaling           min-max              E E P S  4
  scaling           shape                E E G G  1
  backup            frequency            E E P S  2
  backup            retention            E E P S  2
  backup            restore-drill        E E S G  2
  monitoring        metric-set           E E P S  6
  monitoring        alert-threshold      E E E P  6
  network           address-plan         E E S G  1
  network           security-rule        E E P S  9
  network           egress-path          E E S G  2
  identity          user-directory       E E P P  1
  identity          permission-model     E E E E  4
  identity          session-duration     E E P P  3`
  .trim().split("\n").map((r) => {
    const [domain, decision, ...k] = r.trim().split(/\s+/);
    return { domain, decision, touch: +k[4],
      cell: Object.fromEntries(MODEL.map((m, i) => [m, k[i]])) };
  });

export const AREA = [...new Set(DECISION.map((k) => k.domain))];
export const count = (m, ...h) => DECISION.filter((k) => h.includes(k.cell[m])).length;
export const touches = (m) => DECISION.filter((k) => k.cell[m] === "E")
  .reduce((a, k) => a + k.touch, 0);

// Six requirement requests: each touches a single decision.
export const REQUEST = [
  ["upgrade the runtime version", "language-version"],
  ["lower the scaling threshold", "threshold"],
  ["extend backup retention", "retention"],
  ["change the egress path", "egress-path"],
  ["pin the operating-system version", "version"],
  ["shorten the failure-replacement time", "failure-replacement"],
].map(([name, decision]) => ({ name, decision, cell: DECISION.find((k) => k.decision === decision).cell }));
```

```js
// cloud/measure.mjs — how many decisions are handed over in three service models, how many come back.
import { DECISION, MODEL, AREA, REQUEST, WINDOW, count, touches } from "./decision.mjs";

const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const LABEL = { own: "own hw.", infrastructure: "infrastructure", platform: "platform", software: "software" };

console.log(`decision set: ${DECISION.length} decisions, ${AREA.length} areas; ` +
  `${DECISION.reduce((a, k) => a + k.touch, 0)} total decision touches per year`);

const A = [-18, 12, 16, 11, 10];
console.log("\n1. decisions kept per area (decisions in the area / kept)");
write(A, "area", ...MODEL.map((m) => LABEL[m]));
for (const a of AREA) {
  const g = DECISION.filter((k) => k.domain === a);
  write(A, a, ...MODEL.map((m) => `${g.length} / ${g.filter((k) => k.cell[m] === "E").length}`));
}

const B = [-30, 12, 16, 11, 10];
console.log("\n2. decisions handed over and whether they come back");
write(B, "measure", ...MODEL.map((m) => LABEL[m]));
for (const [name, f] of [
  ["decisions kept", (m) => count(m, "E")],
  ["decisions handed over", (m) => count(m, "P", "S", "G")],
  ["  recovered via a parameter", (m) => count(m, "P")],
  ["  waits on a contract window", (m) => count(m, "S")],
  ["  embedded in the service", (m) => count(m, "G")],
  ["recoverable / handed over", (m) => `${count(m, "P")}/${count(m, "P", "S", "G")}`],
  ["kept touches per year", (m) => touches(m)],
  ["touches lifted per handover", (m) =>
    (count(m, "P", "S", "G") ? (touches("own") - touches(m)) / count(m, "P", "S", "G") : 0)
      .toFixed(1)]])
  write(B, name, ...MODEL.map(f));

const C = [-38, 12, 16, 11, 10];
console.log("\n3. can the six requirement requests be met (fictional days; - = cannot be met)");
write(C, "request", ...MODEL.map((m) => LABEL[m]));
const time = (h) => (h === "G" ? "-" : WINDOW[h]);
for (const i of REQUEST) write(C, i.name, ...MODEL.map((m) => time(i.cell[m])));
write(C, "requests met (out of 6)",
  ...MODEL.map((m) => REQUEST.filter((i) => i.cell[m] !== "G").length));
write(C, "total wait of the met ones",
  ...MODEL.map((m) => REQUEST.filter((i) => i.cell[m] !== "G")
    .reduce((a, i) => a + WINDOW[i.cell[m]], 0)));

const D = [-38, 12, 16, 11, 10];
console.log("\n4. what is renegotiated at exit");
write(D, "measure", ...MODEL.map((m) => LABEL[m]));
for (const [name, f] of [
  ["decisions to renegotiate", (m) => count(m, "P", "S", "G")],
  ["  needs a fresh solution (embedded)", (m) => count(m, "G")],
  ["  carries over with the same setting", (m) => count(m, "P", "S")],
  ["carried-over decision share (%)", (m) => Math.round((count(m, "E", "P", "S") / DECISION.length) * 100)]])
  write(D, name, ...MODEL.map(f));
```

```
decision set: 26 decisions, 10 areas; 91 total decision touches per year

1. decisions kept per area (decisions in the area / kept)
area                   own hw.  infrastructure   platform  software
hardware                 3 / 3           3 / 0      3 / 0     3 / 0
facility                 2 / 2           2 / 0      2 / 0     2 / 0
operating-system         2 / 2           2 / 2      2 / 0     2 / 0
patching                 2 / 2           2 / 2      2 / 0     2 / 0
runtime                  3 / 3           3 / 3      3 / 1     3 / 0
scaling                  3 / 3           3 / 3      3 / 0     3 / 0
backup                   3 / 3           3 / 3      3 / 0     3 / 0
monitoring               2 / 2           2 / 2      2 / 1     2 / 0
network                  3 / 3           3 / 3      3 / 0     3 / 0
identity                 3 / 3           3 / 3      3 / 1     3 / 1

2. decisions handed over and whether they come back
measure                            own hw.  infrastructure   platform  software
decisions kept                          26              21          3         1
decisions handed over                    0               5         23        25
  recovered via a parameter              0               2          9         3
  waits on a contract window             0               1          6         5
  embedded in the service                0               2          8        17
recoverable / handed over              0/0             2/5       9/23      3/25
kept touches per year                   91              82         19         4
touches lifted per handover            0.0             1.8        3.1       3.5

3. can the six requirement requests be met (fictional days; - = cannot be met)
request                                    own hw.  infrastructure   platform  software
upgrade the runtime version                      0               0          1         -
lower the scaling threshold                      0               0          1         -
extend backup retention                          0               0          1        30
change the egress path                           0               0         30         -
pin the operating-system version                 0               0          -         -
shorten the failure-replacement time             0               -          -         -
requests met (out of 6)                          6               5          4         1
total wait of the met ones                       0               0         33        30

4. what is renegotiated at exit
measure                                    own hw.  infrastructure   platform  software
decisions to renegotiate                         0               5         23        25
  needs a fresh solution (embedded)              0               2          8        17
  carries over with the same setting             0               3         15         8
carried-over decision share (%)                100              92         69        35
```

The numbers are in the **measurement** class; their inputs are CC7–CC12.

#### The Handover Is Not Linear

The first table shows the handover domain by domain. Infrastructure service takes over only two
domains — hardware and facility — completely; 21 decisions across the remaining eight domains stay
kept. Platform service takes over eight more domains and leaves three items behind: the library
version, the alert threshold, the permission model. In software service, a single decision remains
— the permission model.

The first two rows of the second table reduce this to a single number: decisions handed over
**0 → 5 → 23 → 25**. The jump sits between infrastructure and platform; that single step hands over
eighteen decisions. By contrast, the move from platform to software hands over only two more
decisions. **The service models are not an evenly spaced staircase**; the biggest handover is the
middle step, and that step is usually described as the easiest one to take.

#### Can the Handed-Over Decision Be Taken Back

The number of decisions handed over says nothing on its own; **the recoverability** of what is
handed over is a separate item. In infrastructure, two of the five handovers are recovered via a
parameter, one waits on a contract window, and two are embedded. In platform, of 23 handovers, nine
are parameter, six are window, eight are embedded. In software, of 25 handovers only three are
recovered via a parameter, and **seventeen are embedded**.

The parameter ratio is 2/5, 9/23, and 3/25 in turn — roughly 40%, 39%, and 12%. In the first two
steps the ratio stays nearly constant: platform hands over many decisions but gives back a similar
fraction of them as a setting. In the third step the ratio drops to a third. **In software service,
most of the handed-over decisions were not designed to be taken back**; what the service offers is
precisely that the decision has already been made.

What is received in exchange is in the last two rows. The yearly touches on kept decisions drop
from 91 to 82, 19, and 4. Infrastructure service lifts only 10% of the touches — that is all
handing over five decisions buys. Platform lifts 79% of them. Touches lifted per handover are 1.8,
3.1, and 3.5; by this measure, the least efficient step is infrastructure.

The reason for the difference is visible inside the decision set. At 12 touches a year, the
heaviest item is the security patch, and it stays kept under infrastructure service; the library
version logs 9, the network security rule 9, the alert threshold 6, and the restart 6 touches. Yet
the five decisions infrastructure hands over log a combined nine touches a year. **The weight of a
handed-over decision is measured not by its count but by how many times a year it is touched**;
handing over five decisions lifts nine touches, handing over eighteen decisions lifts sixty-three.

#### When a Requirement Arrives

The third table runs the same six requests across four setups. Under own hardware, all six are met
the same day. Under infrastructure, five are met; the failure-replacement time is no longer the
customer's decision. Under platform, four are met and the total wait climbs to 33 days — three at
one day each, one at thirty days. Under software service **only one of the six requests** is met,
and it too waits thirty days.

This table says the opposite of the second table. Software service is the model that lifts the
most touches per handover (3.5), yet that same model cannot meet five of the six requirements.
**Lifting the operating load is paid for by failing to meet the requirement**; read separately, the
two numbers make the same model look both the best and the worst.

The 33-day wait under platform service is a separate item too. The decision is technically
recoverable, but the speed of recovering it now belongs to the contract, not the customer. A number
that was zero under own hardware climbs to thirty days once the decision is handed over.

The most-touched items in the decision set carry extra weight when making the choice. If a team
touches the alert threshold six times a year, whether that item is `E`, `P`, or `G` matters more
than the other twenty-five decisions in the table combined. **The choice of service model is made
not over the whole decision set, but over the tenth of that set that is touched most often**; the
rest is noticed only once a year.

#### What Is Given Back at Exit

The fourth table counts the exit. The number of decisions to be given back at exit is the number
handed over: 0, 5, 23, 25. Of these, the ones that carry over with the same setting are 3, 15, and
8; **the ones that need a new solution in their place** are 2, 8, and 17. The carried-over decision
share drops from 100% to 92%, 69%, and 35%.

The difference between platform and software is clearest here. The number of decisions handed over
rises by only two, from 23 to 25, but the number embedded climbs from 8 to 17 and the carried-over
share drops from 69% to 35%. **The exit cost is determined not by the number of decisions handed
over, but by the recoverability of what was handed over.**

#### Summary

- In the 26-decision set, decisions handed over run 0 → 5 → 23 → 25; the biggest jump sits between
  infrastructure and platform (eighteen decisions), and the service models are not an evenly spaced
  staircase.
- The share of handovers recoverable via a parameter is 2/5, 9/23, and 3/25; the ratio holds
  through the first two steps, drops to a third in software service, and 17 of the 25 handovers
  are embedded.
- What is received in exchange is a drop in operating load: yearly decision touches run
  91 → 82 → 19 → 4; touches lifted per handover are 1.8, 3.1, and 3.5.
- The number of the six requirement requests that are met is 6, 5, 4, and 1; the total wait is 33
  days under platform and 30 under software. The model that looks most efficient per handover
  cannot meet five of the requests.
- The carried-over decision share at exit runs 100% → 92% → 69% → 35%; while only two more
  decisions are handed over from platform to software, the embedded count climbs from 8 to 17 —
  the exit cost is determined by recoverability, not by count.

#### Next Step

This lesson counted **how much** was rented and split the handed-over decision into three by
recoverability. But every number so far came out under one silent assumption: the rented service is
drawn from a pool open to everyone. Yet the same service type can also be built in a setup reserved
for a single organization, or part of the workload can run in one and the rest in the other. The
next lesson runs the same workload across three deployment setups and compares, item by item, **the
item that stays under control, the fixed cost, the elasticity window**, and the constraint on where
data can reside.

### Deployment Models

Source: https://academia.sh/en/courses/cloud-fundamentals/deployment-models

The measure of public, private, and hybrid deployment: in the same six-component workload, the public layout failing to place two components and six of seven call links failing to be established, control items kept running 8/24, 36/36, 20/36, and 28/36, fixed reserved capacity at 0%, 100%, 45%, and 75%, the elasticity window at 1.0, 21.0, 10.0, and 16.0 days, and hybrid paying 18 operating items, two identity directories, and six mapping items in both of its forms.

The previous lesson measured the **depth** of renting: how the same decision set is split across
three service models. Every number so far came out under one silent assumption — the rented
service is drawn from a pool open to everyone. Yet the same service type can also be built in a
layout reserved for a single organization, or part of the workload can run in one and the rest in
the other.

These three options are named **public cloud**, **private cloud**, and **hybrid cloud**. In public
cloud, resources come from a shared pool; in private cloud, the pool is reserved for a single
organization; in hybrid cloud, the workload is split in two. This lesson sets up the same workload
in all three and counts four items together: **the item that stays under control**, **fixed
reserved capacity**, **the elasticity window**, and the constraint on where data can reside.

#### The Workload and the Residency Class

The example is the course's fictional **regional measurement network**: the reading collector, the
validator, billing, work-order, a nightly batch job, and the subscriber portal. Both the network
and the components are fictional; the placement below is a **model**, and there is no real data
center or contract. Where data can reside is treated here purely as a **type**; no regulation is
named.

**CC13:** the workload is six components and seven call links; each component has a capacity share
and a data residency class. **CC14:** there are three residency classes — unrestricted, within a
bounded limit, on-premises only. **CC15:** the public layout satisfies the first two classes, the
private layout satisfies all three; the hybrid layout satisfies them by splitting the components.
**CC16:** there are six control items; on the public side two of them stay with the customer,
on-premises all six do. **CC17:** the elasticity window is 1 fictional day on the public side and
21 on-premises; fixed reserved capacity is the capacity share of the components that fall
on-premises. **CC18:** the operating-item set is nine and is run separately for each layout that is
set up; if more than one layout is used, an identity mapping item is added for every component.

```js
// cloud/deployment.mjs — the fictional measurement network's placement across three deployment
// layouts (model): no real data center, real regulation, or real contract exists. No randomness.

// residency class: unrestricted < bounded < on-premises (most restrictive)
export const COMPONENT = `
  collector      30  on-premises
  validator      20  bounded
  billing        15  on-premises
  work-order     10  bounded
  nightly-batch  15  unrestricted
  portal         10  bounded`
  .trim().split("\n").map((r) => {
    const [name, share, residency] = r.trim().split(/\s+/);
    return { name, share: +share, residency };
  });

export const LINK = `
  collector validator / validator billing / validator nightly-batch /
  billing work-order / portal billing / portal collector / work-order collector`
  .trim().split("/").map((r) => r.trim().split(/\s+/));

// Four user flows: each flow passes through call links one after another.
export const FLOW = `
  read-flow          collector validator billing
  nightly-flow       collector validator nightly-batch
  subscriber-query   portal billing work-order
  field-flow         work-order collector`
  .trim().split("\n").map((r) => {
    const [name, ...path] = r.trim().split(/\s+/);
    return { name, path };
  });

// Six control items; only two stay with the customer on the public side, all six on-premises.
export const CONTROL = ["physical-access", "hardware-lifespan", "neighboring-tenants",
  "patch-schedule", "log-access", "key-storage-location"];
export const PUBLIC_CONTROL = ["log-access", "key-storage-location"];
export const OPERATIONS = ["patch-pipeline", "monitoring", "backup", "identity", "network-rule",
  "capacity-plan", "log-collection", "release-rollout", "incident-procedure"];
export const WINDOW = { public: 1, private: 21 };   // elasticity window (fictional days)

// Layout: each component lands on the "public" or "private" side; null = cannot be placed.
export const LAYOUT = {
  public: (b) => (b.residency === "on-premises" ? null : "public"),
  private: () => "private",
  "narrow-hybrid": (b) => (b.residency === "on-premises" ? "private" : "public"),
  "wide-hybrid": (b) => (b.residency === "unrestricted" || b.name === "work-order" ? "public" : "private"),
};

export function run(layoutName) {
  const place = Object.fromEntries(COMPONENT.map((b) => [b.name, LAYOUT[layoutName](b)]));
  const placed = COMPONENT.filter((b) => place[b.name]);
  const sides = [...new Set(placed.map((b) => place[b.name]))];
  const totalShare = placed.reduce((a, b) => a + b.share, 0);
  const fixed = placed.filter((b) => place[b.name] === "private").reduce((a, b) => a + b.share, 0);
  const controlKept = placed.reduce((a, b) =>
    a + (place[b.name] === "private" ? CONTROL.length : PUBLIC_CONTROL.length), 0);
  const window = placed.reduce((a, b) => a + b.share * WINDOW[place[b.name]], 0) / totalShare;
  const crossing = LINK.filter(([a, b]) => place[a] && place[b] && place[a] !== place[b]).length;
  const broken = LINK.filter(([a, b]) => !place[a] || !place[b]).length;
  const flow = FLOW.map((f) => {
    if (f.path.some((x) => !place[x])) return { name: f.name, crossings: null };
    let n = 0;
    for (let i = 1; i < f.path.length; i++) if (place[f.path[i - 1]] !== place[f.path[i]]) n++;
    return { name: f.name, crossings: n };
  });
  return { layout: layoutName, flow, sides: sides.length, crossing, broken, directories: sides.length,
    unplaced: COMPONENT.length - placed.length, fixed: Math.round((fixed / totalShare) * 100),
    controlKept, cells: placed.length * CONTROL.length,
    window: window.toFixed(1), operations: OPERATIONS.length * sides.length,
    mapping: sides.length > 1 ? placed.length : 0 };
}
```

```js
// cloud/measure.mjs — same workload in three deployment layouts: control, fixedness, elasticity, boundary cost.
import { run, COMPONENT, LINK, FLOW, LAYOUT, CONTROL, OPERATIONS, WINDOW } from "./deployment.mjs";

const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const L = Object.keys(LAYOUT), R = Object.fromEntries(L.map((l) => [l, run(l)]));

console.log(`workload: ${COMPONENT.length} components, ${LINK.length} call links, ` +
  `${CONTROL.length} control items, ${OPERATIONS.length} operating items; ` +
  `elasticity window public ${WINDOW.public} / private ${WINDOW.private} days`);

const A = [-16, 7, 12, 10, 15, 13];
console.log("\n1. which component lands where (by data residency class)");
write(A, "component", "share", ...L);
for (const b of COMPONENT) write(A, b.name, b.share, ...L.map((l) => LAYOUT[l](b) ?? "unplaced"));

const B = [-37, 10, 12, 15, 13];
console.log("\n2. same workload, item by item");
write(B, "measure", ...L);
for (const [name, f] of [
  ["unplaced components", (r) => r.unplaced],
  ["control items kept (components x 6)", (r) => `${r.controlKept}/${r.cells}`],
  ["fixed reserved capacity (%)", (r) => r.fixed],
  ["elasticity window (days, weighted)", (r) => r.window],
  ["number of layouts set up", (r) => r.sides]])
  write(B, name, ...L.map((l) => f(R[l])));

const C = [-55, 10, 12, 15, 13];
console.log("\n3. hybrid's hidden cost");
write(C, "measure", ...L);
for (const [name, f] of [
  [`call links crossing the boundary (out of ${LINK.length})`, (r) => r.crossing],
  ["links that cannot be established (component unplaced)", (r) => r.broken],
  ["operating items run", (r) => r.operations],
  ["identity directories", (r) => r.directories],
  ["identity mapping items", (r) => r.mapping]])
  write(C, name, ...L.map((l) => f(R[l])));

const F = [-36, 10, 12, 15, 13];
console.log("\n4. how many times each of the four user flows crosses the boundary (- = flow cannot be set up)");
write(F, "flow", ...L);
FLOW.forEach((a, i) => write(F, `${a.name} (${a.path.length} stops)`,
  ...L.map((l) => R[l].flow[i].crossings ?? "-")));
write(F, "flows that can be set up", ...L.map((l) => R[l].flow.filter((a) => a.crossings !== null).length));
write(F, "total boundary crossings", ...L.map((l) =>
  R[l].flow.reduce((a, x) => a + (x.crossings ?? 0), 0)));

const E = [-38, 10, 12, 15, 13];
console.log("\n5. what is rebuilt at exit");
write(E, "measure", ...L);
for (const [name, f] of [
  ["layouts to tear down", (r) => r.sides],
  ["boundary links to rebuild", (r) => r.crossing],
  ["control items that do not carry over", (r) => r.cells - r.controlKept],
  ["control retained share (%)", (r) => Math.round((r.controlKept / r.cells) * 100)]])
  write(E, name, ...L.map((l) => f(R[l])));
```

```
workload: 6 components, 7 call links, 6 control items, 9 operating items; elasticity window public 1 / private 21 days

1. which component lands where (by data residency class)
component         share      public   private  narrow-hybrid  wide-hybrid
collector            30    unplaced   private        private      private
validator            20      public   private         public      private
billing              15    unplaced   private        private      private
work-order           10      public   private         public       public
nightly-batch        15      public   private         public       public
portal               10      public   private         public      private

2. same workload, item by item
measure                                  public     private  narrow-hybrid  wide-hybrid
unplaced components                           2           0              0            0
control items kept (components x 6)        8/24       36/36          20/36        28/36
fixed reserved capacity (%)                   0         100             45           75
elasticity window (days, weighted)          1.0        21.0           10.0         16.0
number of layouts set up                      1           1              2            2

3. hybrid's hidden cost
measure                                                    public     private  narrow-hybrid  wide-hybrid
call links crossing the boundary (out of 7)                     0           0              6            3
links that cannot be established (component unplaced)           6           0              0            0
operating items run                                             9           9             18           18
identity directories                                            1           1              2            2
identity mapping items                                          0           0              6            6

4. how many times each of the four user flows crosses the boundary (- = flow cannot be set up)
flow                                    public     private  narrow-hybrid  wide-hybrid
read-flow (3 stops)                          -           0              2            0
nightly-flow (3 stops)                       -           0              1            1
subscriber-query (3 stops)                   -           0              2            1
field-flow (2 stops)                         -           0              1            1
flows that can be set up                     0           4              4            4
total boundary crossings                     0           0              6            3

5. what is rebuilt at exit
measure                                   public     private  narrow-hybrid  wide-hybrid
layouts to tear down                           1           1              2            2
boundary links to rebuild                      0           0              6            3
control items that do not carry over          16           0             16            8
control retained share (%)                    33         100             56           78
```

The numbers are in the **measurement** class; their inputs are CC13–CC18.

#### The Residency Constraint Is Not a Cost, It Is a Feasibility Gate

The first row of the second table is read before every other row: in the public layout, **two
components cannot be placed at all**. The collector holds raw meter readings, billing holds the
amount together with the subscriber identity; both fall into the on-premises class. Once these two
components cannot be placed, the `links that cannot be established` row in the third table erases
**six** of the seven links — leaving only the link between the validator and the nightly batch job.

This is not a trade-off question of the form "public cloud is cheaper but less controlled." Before
any trade-off is struck there is a feasibility gate: **a single component's residency class can
eliminate an entire layout.** The public layout's 8/24 control items kept and its 1.0-day
elasticity window are technically correct, but they apply to only four components; for the
remaining two there is no number at all.

The private layout satisfies the constraint entirely and keeps **36 of the 36** control items. Its
cost is in the bottom two rows: fixed reserved capacity is 100% and the elasticity window is 21.0
days. So the thirty-day wait the first lesson measured drops to twenty-one under the private
layout, but not to zero; if the resource sits in the organization's own facility, on-demand access
is bounded by the organization's own lead time.

#### Hybrid Is Not a Setting, It Is a Curve

The hybrid layout was set up in two forms. **Narrow hybrid** puts only the two required components
on-premises: fixed capacity 45%, elasticity window 10.0 days, control items kept 20/36. But **six
of the seven call links cross the boundary** — because the two components that fall on-premises
sit at the center of the network, and almost every path runs through them.

**Wide hybrid** also moves the validator and the portal on-premises. Boundary-crossing links drop
from six to **three**, and control items kept rise to 28/36. In exchange, fixed capacity climbs
from 45% to 75%, and the elasticity window from 10.0 to 16.0 days.

Read side by side, the two hybrid columns show that the hybrid layout is not a switch but a
**curve**: the only way to reduce boundary-crossing links is to move more components to the fixed
side, and every component moved to the fixed side grows the elasticity window back. Gaining three
boundary links back costs thirty points of fixed capacity and six days of elasticity window. **In
hybrid, the split line should sit not where the data class draws it, but at the thinnest point of
the call graph** — and these two places do not have to be the same.

#### Hybrid's Fixed Entry Fee

The bottom three rows of the third table are **identical** across both hybrid columns: 18 operating
items, 2 identity directories, 6 identity mapping items. The nine operating items are run
separately for the two layouts; the patch pipeline is set up twice, monitoring twice, backup twice.
Identity splits into two directories, and one mapping item is born for each of the six components.

This is an **entry fee** independent of placement. However well the split line is chosen, moving to
a hybrid layout doubles operating items from nine to eighteen. The two single-layout options —
public and private — write 9, 1, and 0 on these rows. Hybrid offers a middle path between control
and elasticity, but there is a fixed fee at the gate of that middle path, and the fee grows as the
component count grows.

#### Where the Boundary Runs

The fourth table converts the link count into user flows. Under the public layout, **all four**
flows fail to be set up — every flow's path has an unplaceable component sitting on it. The
total-boundary-crossings column reads zero there, but that zero is not a success, it is the result
of there being no flow at all; if the two rows are not read together, the table misleads.

Under narrow hybrid, the read flow crosses the boundary twice, the subscriber query twice, and the
nightly flow and the field flow once each. Under wide hybrid, the read flow never touches the
boundary at all; each of the remaining three flows crosses once. When the split line pulls the
validator onto the on-premises side, **the most frequently run flow collects entirely on one
side**; the price paid is that the validator's 20-unit capacity share is no longer elastic.

#### What Is Rebuilt at Exit

The fifth table counts the exit. Under the single-layout options, one layout must be torn down and
there are no boundary links to rebuild. Under hybrid, two layouts are torn down and the boundary
links — six or three — must be rebuilt from scratch in the new place. Control items that do not
carry over are 16 under public, 16 under narrow hybrid, 8 under wide hybrid, and 0 under private.

The last row turns the course's measurement axis onto this lesson: the control-retained share is
33%, 100%, 56%, and 78%. The lower this share, the more items must be renegotiated at exit. Narrow
hybrid holds 56% control and carries six boundary links; wide hybrid holds 78% and carries three
links — but both have paid the same fixed entry fee.

#### Summary

- The public layout cannot place two of the six components, and six of the seven call links cannot
  be established; the residency class is not a trade-off item but a feasibility gate that comes
  before any trade-off.
- Control items kept are 8/24, 36/36, 20/36, and 28/36; in exchange, fixed reserved capacity is 0%,
  100%, 45%, and 75%, and the elasticity window is 1.0, 21.0, 10.0, and 16.0 days.
- The private layout keeps every control item, but the elasticity window stays at 21.0 days: if the
  resource sits in the organization's own facility, on-demand access is bounded by the
  organization's own lead time.
- Hybrid is not a switch but a curve: dropping boundary-crossing links from 6 to 3 raises fixed
  capacity from 45% to 75% and the elasticity window from 10.0 to 16.0 days. Across the four user
  flows, total boundary crossings drop from 6 to 3, and under wide hybrid the read flow never
  touches the boundary at all.
- Hybrid's hidden cost is independent of placement and identical in both of its forms: 18 operating
  items (the nine run twice), 2 identity directories, 6 identity mapping items.
- At exit, hybrid tears down two layouts and rebuilds either six or three boundary links; the
  control-retained share is 33%, 100%, 56%, and 78%.

#### Next Step

This lesson compared deployment layouts in terms of control, fixedness, and elasticity; no money
changed hands in any of it. Yet the difference between "100% fixed reserved capacity" and "0%"
really shows itself in a payment form: one up front and once, the other continuous and by usage.
The next lesson runs the same workload between capital expense and operating expense, works out the
**break-even point**, and counts where that point shifts as the usage curve changes. Prices are
fictional and are written in a fictional unit.

### Comparison with On-Premises

Source: https://academia.sh/en/courses/cloud-fundamentals/comparison-with-on-premises

The measure of capital expense against operating expense: in the same 36-month usage curve, the break-even month landing at 15, 19, and 13 under the visible total, and at 36, past the window, and 23 under the full total, the items hidden under own hardware holding 64%, 55%, and 48% of the total, and 13% under renting, and the winner shifting as the exit month moves from 12 to 36 — from renting to own hardware in the flat curve, and back to renting in the peak-forming curve. Costs are fictional and are written in a fictional unit.

The previous lesson compared three deployment layouts in terms of control, fixedness, and
elasticity, and no money changed hands anywhere in it. Yet the difference between "100% fixed
reserved capacity" and "0%" really shows itself in a **payment form**: one up front and once, the
other continuous and by usage. This lesson runs those two payment forms on the same usage curve.

The result of the comparison cannot be reduced to a single sentence, because the result depends on
**the shape of the usage curve** and **how long you stay**. These two variables are what is
measured: the month capital expense's cumulative spend drops below operating expense — the
**break-even point** — and where that point shifts as the curve changes.

Every cost in this lesson is **fictional**. There is no real currency, real price list, or real
provider; everything is written in a fictional unit (`fu`). The meaning of the numbers is not in
their size but in their ratios to one another.

#### Two Payment Forms and Six Items

The example is the course's fictional **regional measurement network**: the reading collector, the
validator, billing and work-order services, a nightly batch job, five teams. Both the network and
the services are fictional; the cost structure below is a **model**.

**CC19:** the model runs a 36-month window; every cost is fictional and written in `fu`. **CC20:**
under own hardware, 110 fu is paid once per capacity unit, the smallest purchase is 4 units, and
unit lifespan is 36 months; each installed unit adds 3 fu of operating labor a month, and a 20 fu
facility fee accrues every month the setup stays up. **CC21:** under renting, the unit-month is 12
fu; each unit-month covers 36,000 requests, and every 1,000 requests cost 0.05 fu, meaning 1.8 fu
of request fee is added per unit-month; a one-time 250 fu exit fee accrues at exit. **CC22:**
capacity meets demand every month; own hardware is purchased in four-unit blocks and never reduced,
renting matches demand exactly every month. **CC23:** the visible total counts only the two
visible items — the hardware's upfront cost and the unit-month lease; the full total counts all
six items. **CC24:** at exit, 40% of own hardware's residual book value comes back; under renting,
the exit fee is added once.

```js
// cloud/cost.mjs — the model that runs capital expense against operating expense on the same
// usage curve. All costs are FICTIONAL and are written in a fictional unit (fu); there is no
// real currency, real price list, or real provider. No randomness.

export const MONTHS = 36;
export const F = {
  upfront: 110,   // own hardware: per capacity unit, once (fictional fu)
  block: 4,       // own hardware: smallest purchase
  lifespan: 36,   // own hardware: unit lifespan (months)
  labor: 3,       // own hardware: per installed unit per month (operating labor)
  facility: 20,   // own hardware: fixed per month (power, cooling, siting)
  rent: 12,       // renting: unit-month (on-demand capacity)
  request: 1.8,   // renting: per-request fee per unit-month
  exit: 250,      // renting: once at exit (data transfer)
  recovery: 0.4,  // own hardware: recoverable share of residual book value at exit
};

// Cost items: side, visible (read at first glance in the contract/invoice), description.
export const ITEM = `
  hardware-upfront        own   visible
  operating-labor         own   hidden
  facility-power-cooling  own   hidden
  unit-month-lease        rent  visible
  per-request-fee         rent  hidden
  exit-fee                rent  hidden`
  .trim().split("\n").map((r) => {
    const [name, side, v] = r.trim().split(/\s+/);
    return { name, side, visible: v === "visible" };
  });

const BASE = {
  flat: () => 5,
  peak: (m) => (m >= 14 && m < 20 ? 16 : 5),
  growing: (m) => 3 + 0.4 * m,
};
export const curve = (name) => Array.from({ length: MONTHS }, (_, m) => Math.ceil(BASE[name](m)));

// mode "visible": only the visible items are counted. mode "full": every item is counted.
export function run(curveName, mode) {
  const T = curve(curveName), g = (name) => mode === "full" || ITEM.find((k) => k.name === name).visible;
  const purchases = [];                             // {month, units} own-hardware purchases
  let cap = 0;
  const own = [], rent = [];
  let ownSum = 0, rentSum = 0;
  for (let m = 0; m < MONTHS; m++) {
    while (cap < T[m]) { cap += F.block; purchases.push({ month: m, units: F.block }); }
    if (g("hardware-upfront")) ownSum += purchases.filter((x) => x.month === m).length * F.block * F.upfront;
    if (g("operating-labor")) ownSum += cap * F.labor;
    if (g("facility-power-cooling")) ownSum += F.facility;
    if (g("unit-month-lease")) rentSum += T[m] * F.rent;
    if (g("per-request-fee")) rentSum += T[m] * F.request;
    own.push(ownSum); rent.push(rentSum);
  }
  return { T, purchases, cap, own, rent };
}

// Break-even: the first month (1-based) the own-hardware cumulative spend drops to or below rent.
export function breakEven(r) {
  for (let m = 0; m < MONTHS; m++) if (r.own[m] <= r.rent[m]) return m + 1;
  return null;
}

// Exit: net cost if leaving that month. Under own hardware, a share of the residual book value of
// units purchased so far comes back; under renting, an exit fee is added once.
export function exitCost(r, month, mode) {
  const residual = r.purchases.filter((x) => x.month < month).reduce((s, x) =>
    s + x.units * F.upfront * Math.max(0, F.lifespan - (month - x.month)) / F.lifespan, 0) * F.recovery;
  const extra = mode === "full" ? F.exit : 0;
  return { own: Math.round(r.own[month - 1] - residual), rent: Math.round(r.rent[month - 1] + extra) };
}
```

```js
// cloud/measure.mjs — the break-even point, how it shifts when the usage curve changes, and hidden items.
import { run, breakEven, exitCost, curve, ITEM, F, MONTHS } from "./cost.mjs";

const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const CURVE = ["flat", "peak", "growing"];
const R = Object.fromEntries(CURVE.flatMap((c) => ["visible", "full"]
  .map((mode) => [`${c}-${mode}`, run(c, mode)])));
const month = (b) => (b === null ? `>${MONTHS}` : b);

console.log(`${MONTHS}-month window; all costs are FICTIONAL and are written in a fictional unit (fu). ` +
  `capacity unit: upfront ${F.upfront}, rent ${F.rent}/month`);

const A = [-24, 9, 12, 34];
console.log("\n1. cost items (visible = read at first glance in the contract)");
write(A, "item", "side", "visible", "fictional fee");
const FEE = { "hardware-upfront": `${F.upfront} fu / unit, once`,
  "operating-labor": `${F.labor} fu / installed unit-month`,
  "facility-power-cooling": `${F.facility} fu / month`,
  "unit-month-lease": `${F.rent} fu / unit-month`,
  "per-request-fee": `${F.request} fu / unit-month`,
  "exit-fee": `${F.exit} fu, once` };
for (const k of ITEM) write(A, k.name, k.side, k.visible ? "yes" : "no", FEE[k.name]);

const B = [-10, 12, 12, 10, 15, 13];
console.log("\n2. break-even month: when own hardware overtakes renting");
write(B, "curve", "lowest", "highest", "purchase", "visible total", "full total");
for (const c of CURVE) {
  const T = curve(c);
  write(B, c, Math.min(...T), Math.max(...T), R[`${c}-full`].cap,
    month(breakEven(R[`${c}-visible`])), month(breakEven(R[`${c}-full`])));
}

const C = [-10, 13, 11, 13, 14, 11, 13];
console.log(`\n3. cumulative spend at month ${MONTHS} (fu) and the hidden share`);
write(C, "curve", "own visible", "own full", "hidden (%)", "rent visible", "rent full",
  "hidden (%)");
const share = (v, f) => `${Math.round(f - v)} (${Math.round(((f - v) / f) * 100)})`;
for (const c of CURVE) {
  const v = R[`${c}-visible`], f = R[`${c}-full`], i = MONTHS - 1;
  write(C, c, v.own[i], Math.round(f.own[i]), share(v.own[i], f.own[i]),
    v.rent[i], Math.round(f.rent[i]), share(v.rent[i], f.rent[i]));
}

const D = [-10, 12, 12, 12, 12, 12];
console.log(`\n4. net cost by exit month under the full ledger (fu; ` +
  `${F.recovery * 100}% of residual book value comes back)`);
write(D, "curve", "exit month", "own", "rent", "gap", "winner");
for (const c of CURVE) for (const m of [12, 24, 36]) {
  const e = exitCost(R[`${c}-full`], m, "full");
  write(D, c, m, e.own, e.rent, e.own - e.rent, e.own < e.rent ? "own" : "rent");
}
```

```
36-month window; all costs are FICTIONAL and are written in a fictional unit (fu). capacity unit: upfront 110, rent 12/month

1. cost items (visible = read at first glance in the contract)
item                         side     visible                     fictional fee
hardware-upfront              own         yes               110 fu / unit, once
operating-labor               own          no       3 fu / installed unit-month
facility-power-cooling        own          no                     20 fu / month
unit-month-lease             rent         yes                12 fu / unit-month
per-request-fee              rent          no               1.8 fu / unit-month
exit-fee                     rent          no                      250 fu, once

2. break-even month: when own hardware overtakes renting
curve           lowest     highest  purchase  visible total   full total
flat                 5           5         8             15           36
peak                 5          16        16             19          >36
growing              3          17        20             13           23

3. cumulative spend at month 36 (fu) and the hidden share
curve       own visible   own full   hidden (%)  rent visible  rent full   hidden (%)
flat                880       2464    1584 (64)          2160       2484     324 (13)
peak               1760       3872    2112 (55)          2952       3395     443 (13)
growing            2200       4216    2016 (48)          4488       5161     673 (13)

4. net cost by exit month under the full ledger (fu; 40% of residual book value comes back)
curve       exit month         own        rent         gap      winner
flat                12        1173        1078          95        rent
flat                24        1819        1906         -87         own
flat                36        2464        2734        -270         own
peak                12        1173        1078          95        rent
peak                24        2684        2817        -133         own
peak                36        3735        3645          90        rent
growing             12        1123        1175         -52         own
growing             24        2499        2900        -401         own
growing             36        3864        5411       -1547         own
```

The numbers are in the **measurement** class; their inputs are CC19–CC24, and all of them are
fictional costs.

#### The Break-Even Point

The second table runs the same calculation twice. **The visible total** counts only the two items
read at first glance in the contract: the hardware's upfront cost and the unit-month lease. Under
this count, break-even is **month 15** in the flat curve, month 19 in the peak-forming curve,
month 13 in the growing curve. These three numbers are where the sentence "it pays for itself in
two years" comes from.

**The full total** counts all six items, and across the same three curves break-even shifts to
month 36, past the window, and month 23. The shift is 21 months in the flat curve, past the window
in the peak-forming curve, 10 months in the growing curve. **The break-even point is not a fact
but a result of which items were taken into the count**; the gap between the two totals shows that
the sides of the argument are often not measuring the same thing.

#### Where Break-Even Shifts When the Curve Changes

The three curves run the same service under three different demand shapes. **In the flat curve**,
demand is 5 units for all 36 months; own hardware buys 8 units because of the four-unit block, and
carries the extra for its entire lifespan. Under the full total, break-even sits right at the end
of the window — meaning it only breaks even if you stay for the full three years.

**In the peak-forming curve**, demand climbs from 5 to 16 units over six months and then drops
back. Own hardware climbs to 16 units to cover that peak and keeps holding 16 even after the peak
has passed; renting pays through the peak and then comes back down. Under the full total,
break-even falls outside the window. **A single six-month peak reverses a three-year investment
decision.**

**In the growing curve**, demand climbs steadily from 3 to 17 units. Here own hardware gives the
earliest break-even (month 23), because renting pays a little more every month and total rent
reaches 5,161 fu by month 36 — the highest of the three curves. **Where renting costs you is a
predictably growing load**; there, nothing is gained from elasticity, because capacity never comes
back down in the first place.

#### Hidden Items

The third table writes the gap between the two totals as an item. Under own hardware at month 36,
the visible total is 880 fu in the flat curve and the full total 2,464 fu: **1,584 fu hidden, 64%
of the total**. It is 55% in the peak-forming curve and 48% in the growing curve. The hidden items
have known names — operating labor per installed unit and the monthly facility fee — and neither
shows up in a purchase decision, because neither appears on the hardware invoice.

Under renting the same share is **13%** across all three curves, and its only source is the
per-request fee. The share being low and constant does not mean renting is more honest; renting's
biggest hidden item is not in this table at all, because that item is born only **at exit**.

The direction the hidden share moves from curve to curve says something too. Under own hardware
the share is highest in the flat curve (64%) and lowest in the growing curve (48%) — because as
upfront hardware spending grows in the growing curve, the visible item's share grows with it. In
other words, **the hidden part of the ledger is largest under low usage**: when hardware is used
lightly, the upfront cost stays small, but operating labor and the facility fee run at full
strength from day one. Under renting, the share staying independent of the curve is because that
side's hidden item is proportional to usage.

#### The Exit Month Changes the Winner

The fourth table cuts the same three curves at three different exit months and counts the exit
items too: under own hardware, 40% of residual book value comes back; under renting, a 250 fu exit
fee is added.

In the flat curve, renting leads by 95 fu at month 12, own hardware leads by 87 fu at month 24, and
by 270 fu at month 36. In the peak-forming curve the lead flips twice: renting at month 12, own
hardware at month 24, renting again at month 36 (by 90 fu). In the growing curve own hardware leads
at all three cuts, and the gap widens to 1,547 fu by month 36.

The peak-forming curve flipping direction twice is this lesson's main finding. There is no correct
answer to the decision; **what determines the answer is how long you stay**, and that duration is
unknown at decision time. What own hardware loses on early exit is the 60% of residual book value
that never comes back; what renting loses on late exit is the accumulated unit-month lease. Both
sides are buying a kind of lock-in: one to an asset, the other to a contract.

#### Summary

- The break-even month is 15, 19, and 13 under the visible total; 36, past the window, and 23
  under the full total. The shift is 21 months, past the window, and 10 months in turn; break-even
  is not a fact but a result of which items are taken into the count.
- Under own hardware, hidden items hold 64%, 55%, and 48% of the total at month 36; under renting
  the same share is 13% across all three curves, and its only source is the per-request fee.
- In the peak-forming curve, own hardware climbs to 16 units and keeps holding them even after the
  peak passes; a single six-month peak throws the full-total break-even outside the 36-month
  window.
- Where renting costs you is a predictably growing load: there, total rent climbs to 5,161 fu by
  month 36, and break-even drops to month 23.
- The exit month changes the winner: in the flat curve, renting at month 12, own hardware at
  months 24 and 36; in the peak-forming curve the lead flips twice (renting at month 12, own
  hardware at month 24, renting again at month 36).
- The hidden items have known names — operating labor and facility under own hardware, the
  per-request fee and the exit fee under renting; neither appears on the document the decision is
  made from.

#### Next Step

This lesson turned renting's cost into money and showed that the winner changes depending on which
items the count takes in. But one item was never asked throughout the whole table: when something
goes wrong in the rented service, **who was supposed to have done it**. The operating-labor item is
a single number under own hardware; under renting, part of that labor passes to the provider — but
exactly which part? The next lesson builds a **responsibility matrix**: item by item, which side,
how many items fall into the **gap**, and how many **overlap**.

### Shared Responsibility Model

Source: https://academia.sh/en/courses/cloud-fundamentals/shared-responsibility-model

The measure of the boundary between provider and customer: in a 24-item responsibility matrix, items passing to the provider climbing from 6 to 16 and 20, items falling in the gap at 2, 4, and 5, overlapping items at 2, 4, and 2, eight incidents closing in a total of 56, 73, and 68 rounds, with 28, 35, and 35 of those rounds coming from the gap alone, 4, 5, and 4 of eight incidents closing with data loss, and carried-over items at exit dropping from 18 to 8 and 4.

The previous lesson turned renting's cost into money and counted one item by name: **operating
labor**. Under own hardware that labor is a single number, and all of it belongs to the
organization. Under renting, part of it passes to the provider — but exactly which part? This
lesson draws that boundary and counts what sits not on either side of the line, but **on it**.

**Shared responsibility** is the split, between provider and customer, of the work required to
keep a service up and secure. It takes the same shape as M20's **handoff agreement**, but the
boundary sits elsewhere: there it split between the team that writes the work and the team that
runs it, and both sides belonged to the same organization; here, the other side of the boundary is
a contract, and the other party does not sit at the table. This difference produces one new class:
an item can belong to **no one at all**.

#### The Responsibility Matrix

The example is the course's fictional **regional measurement network**: the reading collector, the
validator, billing and work-order services, a nightly batch job, five teams. Both the network and
the services are fictional; the matrix below is a **model**, and there is no real provider,
contract, or incident record.

**CC25:** the responsibility matrix is 24 items; in each of the three service models, every item
sits with the provider, with the customer, or is shared. **CC26:** a shared item behaves in one of
three ways — **solid** (both sides take their own share), **gap** (both sides think the other does
it, and no one does), **overlap** (both do it, and which one governs is unclear); this tendency
belongs to the item and is independent of the model. **CC27:** there are eight incidents, and each
incident touches three items. **CC28:** the incident's rounds depend on the item's class — 1 at the
provider, 1 at the customer, 2 for a solid shared item, 3 for an overlap, 8 for a gap; a gap item
is built from scratch during the incident. **CC29:** an incident that touches even one gap item
closes with data loss. **CC30:** at exit, items that stay with the customer carry over; items at
the provider or shared are renegotiated with the new provider.

```js
// cloud/responsibility.mjs — the shared responsibility matrix for the fictional measurement
// network (model): no real provider, real contract, or real incident record exists. No randomness.

export const MODEL = ["infrastructure", "platform", "software"];
// Columns: infrastructure platform software. S at the provider, M at the customer, P shared.
// tendency: how a shared item behaves — solid / gap / overlap.
export const ITEM = `
  physical-security        S S S  solid
  host-patch               S S S  solid
  guest-os-patch           M S S  solid
  application-patch        M M S  solid
  network-rule             P P S  gap
  edge-protection          P P S  overlap
  encryption-at-rest       M P S  solid
  encryption-in-transit    P P S  overlap
  key-management           M P P  gap
  identity-and-access      M M M  solid
  backup-creation          M P S  solid
  restore-drill            M M M  gap
  log-collection           M P P  solid
  log-retention            M M P  gap
  alert-threshold          M M M  solid
  incident-first-response  M P P  overlap
  incident-root-cause      P P P  gap
  capacity                 M P S  solid
  image-distribution       M S S  solid
  configuration-audit      M M P  gap
  data-classification      M M M  solid
  data-deletion            M P P  gap
  component-scanning       M M P  solid
  disaster-drill           M P P  overlap`
  .trim().split("\n").map((r) => {
    const [name, ...k] = r.trim().split(/\s+/);
    return { name, tendency: k[3], cell: Object.fromEntries(MODEL.map((m, i) => [m, k[i]])) };
  });

// Resolution: if the contract value is P, the item's tendency decides the class.
export const classify = (k, m) =>
  (k.cell[m] === "P" ? { solid: "sha", gap: "GAP", overlap: "OVE" }[k.tendency]
    : k.cell[m] === "S" ? "pro" : "cus");
export const CLASS = ["pro", "cus", "sha", "GAP", "OVE"];
export const count = (m, c) => ITEM.filter((k) => classify(k, m) === c).length;

// Incident rounds: however many rounds the item's class takes. A gap item is rebuilt from scratch mid-incident.
export const ROUND = { pro: 1, cus: 1, sha: 2, OVE: 3, GAP: 8 };

export const INCIDENT = `
  leak-suspicion      log-collection log-retention incident-root-cause
  data-loss           backup-creation restore-drill data-classification
  external-scan       network-rule edge-protection incident-first-response
  key-expiry          encryption-in-transit key-management alert-threshold
  unauthorized-access identity-and-access log-collection incident-root-cause
  patch-gap           guest-os-patch component-scanning image-distribution
  misconfiguration    configuration-audit network-rule incident-first-response
  broad-outage        disaster-drill capacity restore-drill`
  .trim().split("\n").map((r) => {
    const [name, ...k] = r.trim().split(/\s+/);
    return { name, item: k.map((x) => ITEM.find((y) => y.name === x)) };
  });

export function run(m) {
  const incidents = INCIDENT.map((o) => {
    const c = o.item.map((k) => classify(k, m));
    return { name: o.name, rounds: c.reduce((a, x) => a + ROUND[x], 0),
      gaps: c.filter((x) => x === "GAP").length, overlaps: c.filter((x) => x === "OVE").length };
  });
  const total = (f) => incidents.reduce((a, o) => a + f(o), 0);
  return { incidents, rounds: total((o) => o.rounds), dataLoss: incidents.filter((o) => o.gaps > 0).length,
    gapRounds: total((o) => o.gaps * (ROUND.GAP - 1)), overlapRounds: total((o) => o.overlaps * (ROUND.OVE - 1)),
    carried: count(m, "cus"), renegotiate: ITEM.length - count(m, "cus") };
}
```

```js
// cloud/measure.mjs — where the boundary sits, how many items fall in the gap, how many overlap, what happens in an incident.
import { ITEM, MODEL, INCIDENT, CLASS, ROUND, classify, count, run } from "./responsibility.mjs";

const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const R = Object.fromEntries(MODEL.map((m) => [m, run(m)]));

console.log(`responsibility matrix: ${ITEM.length} items x 3 service models; ` +
  `${INCIDENT.length} incidents, 3 items per incident; incident round ` +
  CLASS.map((c) => `${c} ${ROUND[c]}`).join(", "));

const A = [-25, 16, 11, 10];
console.log("\n1. responsibility matrix (pro at the provider, cus at the customer, sha shared,");
console.log("   GAP both sides think the other does it, OVE both sides do it)");
write(A, "item", ...MODEL);
for (const k of ITEM) write(A, k.name, ...MODEL.map((m) => classify(k, m)));

const B = [-42, 16, 11, 10];
console.log("\n2. where the boundary shifts across the three models");
write(B, "class", ...MODEL);
for (const c of CLASS) write(B, c, ...MODEL.map((m) => count(m, c)));
write(B, "passes to the provider (pro+sha+GAP+OVE)", ...MODEL.map((m) => ITEM.length - count(m, "cus")));
write(B, "problem items (GAP+OVE)", ...MODEL.map((m) => count(m, "GAP") + count(m, "OVE")));

const C = [-34, 16, 11, 10];
console.log("\n3. how many rounds the eight incidents take to close (parens: gap items)");
write(C, "incident", ...MODEL);
INCIDENT.forEach((o, i) => write(C, o.name,
  ...MODEL.map((m) => `${R[m].incidents[i].rounds} (${R[m].incidents[i].gaps})`)));
for (const [name, f] of [["total rounds", (r) => r.rounds],
  ["  extra rounds from gaps", (r) => r.gapRounds],
  ["  extra rounds from overlaps", (r) => r.overlapRounds],
  ["incidents closing with data loss", (r) => `${r.dataLoss}/${INCIDENT.length}`]])
  write(C, name, ...MODEL.map((m) => f(R[m])));

const D = [-33, 16, 11, 10];
console.log("\n4. what carries over at exit");
write(D, "measure", ...MODEL);
for (const [name, f] of [["carried-over items (customer)", (m) => R[m].carried],
  ["items to renegotiate", (m) => R[m].renegotiate],
  ["  of these, already a problem", (m) => count(m, "GAP") + count(m, "OVE")],
  ["carried-over item share (%)", (m) => Math.round((R[m].carried / ITEM.length) * 100)]])
  write(D, name, ...MODEL.map(f));
```

```
responsibility matrix: 24 items x 3 service models; 8 incidents, 3 items per incident; incident round pro 1, cus 1, sha 2, GAP 8, OVE 3

1. responsibility matrix (pro at the provider, cus at the customer, sha shared,
   GAP both sides think the other does it, OVE both sides do it)
item                       infrastructure   platform  software
physical-security                     pro        pro       pro
host-patch                            pro        pro       pro
guest-os-patch                        cus        pro       pro
application-patch                     cus        cus       pro
network-rule                          GAP        GAP       pro
edge-protection                       OVE        OVE       pro
encryption-at-rest                    cus        sha       pro
encryption-in-transit                 OVE        OVE       pro
key-management                        cus        GAP       GAP
identity-and-access                   cus        cus       cus
backup-creation                       cus        sha       pro
restore-drill                         cus        cus       cus
log-collection                        cus        sha       sha
log-retention                         cus        cus       GAP
alert-threshold                       cus        cus       cus
incident-first-response               cus        OVE       OVE
incident-root-cause                   GAP        GAP       GAP
capacity                              cus        sha       pro
image-distribution                    cus        pro       pro
configuration-audit                   cus        cus       GAP
data-classification                   cus        cus       cus
data-deletion                         cus        GAP       GAP
component-scanning                    cus        cus       sha
disaster-drill                        cus        OVE       OVE

2. where the boundary shifts across the three models
class                                       infrastructure   platform  software
pro                                                      2          4        11
cus                                                     18          8         4
sha                                                      0          4         2
GAP                                                      2          4         5
OVE                                                      2          4         2
passes to the provider (pro+sha+GAP+OVE)                 6         16        20
problem items (GAP+OVE)                                  4          8         7

3. how many rounds the eight incidents take to close (parens: gap items)
incident                            infrastructure   platform  software
leak-suspicion                              10 (1)     11 (1)    18 (2)
data-loss                                    3 (0)      4 (0)     3 (0)
external-scan                               12 (1)     14 (1)     5 (0)
key-expiry                                   5 (0)     12 (1)    10 (1)
unauthorized-access                         10 (1)     11 (1)    11 (1)
patch-gap                                    3 (0)      3 (0)     4 (0)
misconfiguration                            10 (1)     12 (1)    12 (1)
broad-outage                                 3 (0)      6 (0)     5 (0)
total rounds                                    56         73        68
  extra rounds from gaps                        28         35        35
  extra rounds from overlaps                     4         10         6
incidents closing with data loss               4/8        5/8       4/8

4. what carries over at exit
measure                            infrastructure   platform  software
carried-over items (customer)                  18          8         4
items to renegotiate                            6         16        20
  of these, already a problem                   4          8         7
carried-over item share (%)                    75         33        17
```

The numbers are in the **measurement** class; their inputs are CC25–CC30.

#### Where the Boundary Shifts

The second table reads the boundary across the three models. Items sitting with the provider climb
from 2 to 4 and 11; items staying with the customer drop from 18 to 8 and 4. The total that passes
to the provider's side — provider, shared, gap, and overlap combined — is 6, 16, and 20. Up to this
point, the table looks the same direction as the previous lessons' count of decisions handed over.

The real finding is in the last row. **The count of problem items — gap plus overlap — is 4, 8,
and 7.** The highest count sits not in the model that hands over the most, but in the **middle**
model. The reason is the third row: the count of shared items is 0, 4, and 2, and gap and overlap
are born only in shared items. Under infrastructure service almost everything belongs to the
customer, under software service almost everything belongs to the provider; in the middle, half
the items face both sides at once.

**Uncertainty piles up not at the ends of the boundary but where it shifts.** `incident-root-cause`
is in the gap in all three models: the provider cannot see below its own layer, and the customer
cannot see inside the provider's. `edge-protection` and `encryption-in-transit` are in overlap
under infrastructure and platform — since both sides do it, no one notices its absence, but which
setting governs gets asked the moment there is an incident.

#### What a Gap Does in an Incident

The third table runs the matrix over the eight incidents. Total closing rounds are 56 under
infrastructure, 73 under platform, 68 under software. The rows below write where those rounds come
from: an ordinary item takes one round, a gap item eight; **the extra rounds from gaps are 28, 35,
and 35**, from overlaps 4, 10, and 6.

The infrastructure column gives this lesson's sharpest number. **Two** of the twenty-four items are
in the gap, and those two items write **exactly half** of the eight incidents' 56 total rounds. A
gap item is not a missing document; it is work built from scratch at the moment of the incident,
and it does not stop the incident's clock.

Looking at individual incidents makes the effect clearer. `leak-suspicion` touches two gap items at
once under software service and closes in 18 rounds; the same incident is 10 rounds under
infrastructure. By contrast, `data-loss` and `patch-gap` never touch a gap in any model and close
in three to four rounds. Whether an incident is expensive is determined not by the incident itself
but by **where the items it touches fall in the matrix**.

The last row converts the cost into a non-technical unit: **four, five, and four of the eight
incidents close with data loss.** This count is nearly identical across the three models.
Deepening the service model raises the gap count from 2 to 5 but does not lower the number of
incidents closing with data loss — because gap items sit scattered across the incident paths, not
collected in one place.

#### Overlap Is Cheaper Than a Gap

The two problem classes do not carry the same weight. An overlapping item writes two extra rounds
per incident, a gap item seven. Under platform, the matrix contains four overlapping and four gap
items; across eight incidents, overlap writes 10 extra rounds, gap 35 — the item counts are equal,
the costs three and a half times apart.

Their directions are opposite, too. As the handover deepens, overlap climbs from 2 to 4 and then
drops back to 2, while gap keeps rising — 2, 4, 5. When the provider clearly takes on an item,
overlap disappears; **an item that is not clearly taken on does not disappear, it falls into the
gap.** In a responsibility matrix the dangerous cell is not the one with two marks in it, but the
one with **no mark at all** — and that cell gets skipped during review, because a row that looks
empty in the table reads like an incomplete record, not like an unclaimed obligation.

This distinction also determines the order of review. An overlapping item stands out at first
glance, because it is work both sides do: there are two separate settings, two separate records,
two separate invoice lines. A gap item leaves no trace at all — no setting, no record, no owner.
Finding the four overlapping items in the matrix takes an afternoon; finding the four gaps requires
asking both sides about every item, one at a time. **The value of a responsibility matrix shows up
not in its filled cells, but when its empty cells are searched for.**

#### What Carries Over at Exit

The fourth table closes the course's measurement axis. Items that stay with the customer, and so
carry over unchanged to a new setup, are 18, 8, and 4 — 75%, 33%, and 17% of the 24 items in turn.
Items to renegotiate are 6, 16, and 20.

The last row shows the hidden part of the exit: 4, 8, and 7 of the items to renegotiate are
**already a problem**. So what carries over on exit is not just a responsibility list, but the
undefined cells in that list; an item stuck in the gap tends to stay in the gap under a new
provider too, because the cause of the gap is not the provider but the item sitting outside both
sides' field of view.

#### Summary

- In the 24-item matrix, items passing to the provider are 6, 16, and 20; items staying with the
  customer are 18, 8, and 4.
- Gap items are 2, 4, and 5; overlapping items are 2, 4, and 2. The total of problem items (4, 8,
  7) peaks not in the model that hands over the most, but **in the middle model, where the
  boundary shifts the most**.
- Eight incidents close in 56, 73, and 68 rounds; 28, 35, and 35 of those rounds come from gaps
  alone. Under infrastructure, two gap items write exactly half of the total rounds.
- 4, 5, and 4 of the eight incidents close with data loss; deepening the service model does not
  lower this count, because gap items sit scattered across the incident paths.
- Overlap is cheaper than a gap: with equal item counts, overlap produces 10 rounds under platform
  and gap 35. As the handover deepens, overlap shrinks and gap grows.
- The carried-over item share at exit is 75%, 33%, and 17%; 4, 8, and 7 of the items to
  renegotiate are already in a gap or an overlap.

#### Next Step

This topic measured renting five times: how quickly capacity arrives, how many decisions are
handed over, which layout the workload fits into, how many items the ledger hides, and where the
boundary of responsibility runs. The boundary is now drawn, and who does what — and what no one
does — is written down.

Yet across all these measurements, one question was never asked: **where the rented thing
physically sits**. The `physical-security` row in the responsibility matrix sits with the provider
in all three models, but which geography, how many separate locations, and what failure boundaries
the provider holds that item under was never asked. When a failure happens, what determines what
stays standing is not the responsibility table but **where the rented resource sits**. The next
topic begins with that question.

## Infrastructure Geography and Design Principles

Where the rented capacity sits and how it is priced: separating the region from the availability zone as a failure domain, edge locations for proximity to the user, how the pillars of the design framework constrain one another, the price-and-risk balance of on-demand, reserved, and spot capacity, and the cost of the rehost, replatform, and refactor migration strategies.

### Regions and Availability Zones

Source: https://academia.sh/en/courses/cloud-fundamentals/regions-and-availability-zones

Counting where the rented item physically sits: the item a single-zone placement leaves standing after a zone loss, the extra capacity cost of spreading across two and three zones, how a region loss stays independent of zone spread, and the time cross-region latency adds to the nightly batch job.

The previous lesson drew the responsibility boundary: which item's upkeep sits with the
provider, which with the tenant, and which sits in the gap where each side assumes the
other handles it. The boundary was drawn, but one question was left unasked — where the
rented item **physically sits**. A lease does not say which building or which power line
it sits on; yet how much of the service stays standing when that line is cut depends on
this information.

An **availability zone** is a placement unit with its own power, cooling, and network
entry, separated so that it does not go down at the same time as its neighbor. A
**region** is a cluster of such zones connected by low-latency links. The difference
between the two is not a geography question but a **failure-domain separation** question:
if two items are in the same zone, the same failure takes both together; if they are in
separate zones, it does not. What this lesson measures is the payoff and the cost of that
separation. The deployment stamp and geographic replica measurement were done in the
Resilience and Reliability course; they are not repeated here.

#### The Placement Model

The setup below is a **model**. The items of the fictional regional measurement network —
the water-meter reading collector, the validator, billing, the work-order service, the
stores, and the nightly batch job — are labeled by their scope. There is no real region,
zone, or provider; the capacity numbers are in a fictional `unit/hour`.

- **CG1.** In this model a region consists of three availability zones with independent power,
  cooling, and network entry. With real providers the number of zones — and whether a region has
  zones at all — varies from provider to provider.
- **CG2.** The network has 13 items, and each item is zone-scoped, region-scoped, or
  global-scoped.
- **CG3.** The tenant chooses which zone a zone-scoped item is placed in. The provider
  distributes region- and global-scoped items; the distribution detail is invisible to the
  tenant and cannot be tested.
- **CG4.** A zone loss takes every item in that zone at the same time; two zones do not go
  down at the same time.

```js
// geography/model.mjs — region/zone placement is a MODEL: the fictional measurement network's items,
// scopes, and base capacities. There is no real region, zone, or provider; every number is fictional.
export const ZONE = ["a1", "a2", "a3"];        // separate failure zones in a region (fictional)

// name, scope, base capacity (fictional unit/hour), write endpoint singular?
export const ITEM = [
  ["read-collector", "zone", 12, false], ["meter-endpoint", "zone", 6, false],
  ["billing", "zone", 4, false], ["work-order-service", "zone", 3, false],
  ["read-store", "zone", 8, true], ["tariff-store", "zone", 2, true],
  ["nightly-batch-job", "zone", 5, true], ["validator", "region", 6, false],
  ["raw-read-store", "region", 3, false], ["read-queue", "region", 2, false],
  ["load-balancer", "region", 2, false], ["key-management", "region", 1, false],
  ["name-resolution", "global", 1, false],
];

export const scoped = (k) => ITEM.filter(([, k2]) => k2 === k);
export const BASE = scoped("zone").reduce((a, [, , c]) => a + c, 0);          // 40 units
export const REGION_RENT = scoped("region").reduce((a, [, , c]) => a + c, 0); // distribution is the provider's
export const GLOBAL_RENT = scoped("global").reduce((a, [, , c]) => a + c, 0);

// surviving base capacity share after a zone loss, in a placement spread over n zones
export const survivingShare = (n) => (n - 1) / n;
// to restore full capacity after a loss, base/(n-1) is placed in each zone; total rented = BASE*n/(n-1)
export const multiplier = (n) => (n < 2 ? Infinity : n / (n - 1));

export function measurePlan(n) {
  const zoneItems = scoped("zone").length;
  const standingItems = n < 2 ? 0 : zoneItems;        // no copy in a single zone, all in the same zone
  const full = n < 2 ? Infinity : BASE * multiplier(n);
  return {
    n, standingItems, zoneItems, survivingShare: survivingShare(n),
    lean: BASE + REGION_RENT + GLOBAL_RENT,             // rented without buying extra
    full: n < 2 ? Infinity : full + REGION_RENT + GLOBAL_RENT,
    extra: n < 2 ? Infinity : full - BASE,
    writeCut: scoped("zone").filter(([, , , t]) => t).length, // write endpoint is single-zone
  };
}
```

#### When a Zone Goes Down

The measure reads two numbers together: what is **left standing** after the loss, and
what is **rented extra** to restore full capacity. Without the second, the first carries
no decision.

```js
// geography/measure.mjs — item standing after a zone loss, standing capacity, and the
// extra cost of restoring full capacity; both rise together as zone count grows
import { ITEM, BASE, REGION_RENT, GLOBAL_RENT, scoped, measurePlan, multiplier } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x) => (Number.isFinite(x) ? x.toFixed(2) : "-");
const NAME = { 1: "single-zone", 2: "two-zone", 3: "three-zone", 4: "four-zone" };

console.log(`${ITEM.length} items: ${scoped("zone").length} zone-scoped, ` +
  `${scoped("region").length} region-scoped, ${scoped("global").length} global.`);
console.log(`zone-scoped base capacity ${BASE} units; region-scoped rent ${REGION_RENT}, ` +
  `global rent ${GLOBAL_RENT} units (fictional).`);
console.log(`items whose zone choice is not made by the tenant: ${ITEM.length - scoped("zone").length}`);
console.log();

console.log("plan          | zones   | standing items   | standing capacity   | lean rent   | full-capacity rent   | extra   ");
console.log("--------------|---------|------------------|---------------------|-------------|----------------------|---------");
for (const n of [1, 2, 3, 4]) {
  const p = measurePlan(n);
  console.log(`${NAME[n].padEnd(13)} | ${s(n, 7)} | ${s(`${p.standingItems}/${p.zoneItems}`, 16)} | ` +
    `${s(`${(100 * p.survivingShare).toFixed(1)}%`, 19)} | ${s(y(p.lean), 11)} | ` +
    `${s(y(p.full), 20)} | ${s(y(p.extra), 8)}`);
}

console.log();
console.log("zones   | multiplier   | extra units    | gain vs previous plan       ");
console.log("--------|--------------|----------------|-----------------------------");
let previousExtra = null;
for (const n of [2, 3, 4, 5, 6]) {
  const f = BASE * multiplier(n) - BASE;
  const gain = previousExtra === null ? "-" : y(previousExtra - f);
  console.log(`${s(n, 7)} | ${s(y(multiplier(n)), 12)} | ${s(y(f), 14)} | ${s(gain, 28)}`);
  previousExtra = f;
}

console.log();
const p2 = measurePlan(2);
console.log(`items whose write endpoint is cut by a zone loss: ${p2.writeCut}/${scoped("zone").length}` +
  ` — spreading rescues reads, not the single write endpoint.`);
```

```
13 items: 7 zone-scoped, 5 region-scoped, 1 global.
zone-scoped base capacity 40 units; region-scoped rent 14, global rent 1 units (fictional).
items whose zone choice is not made by the tenant: 6

plan          | zones   | standing items   | standing capacity   | lean rent   | full-capacity rent   | extra   
--------------|---------|------------------|---------------------|-------------|----------------------|---------
single-zone   |       1 |              0/7 |                0.0% |       55.00 |                    - |        -
two-zone      |       2 |              7/7 |               50.0% |       55.00 |                95.00 |    40.00
three-zone    |       3 |              7/7 |               66.7% |       55.00 |                75.00 |    20.00
four-zone     |       4 |              7/7 |               75.0% |       55.00 |                68.33 |    13.33

zones   | multiplier   | extra units    | gain vs previous plan       
--------|--------------|----------------|-----------------------------
      2 |         2.00 |          40.00 |                            -
      3 |         1.50 |          20.00 |                        20.00
      4 |         1.33 |          13.33 |                         6.67
      5 |         1.25 |          10.00 |                         3.33
      6 |         1.20 |           8.00 |                         2.00

items whose write endpoint is cut by a zone loss: 3/7 — spreading rescues reads, not the single write endpoint.
```

The first row is this lesson's harshest number: in a single-zone placement, when a zone
goes down the item left standing is **0/7** and the standing capacity is **0 percent**.
No redundancy or restart policy changes this row: what is lost is not the copy, but the
**single** failure domain the copies sit in.

Spread across two zones, the item left standing becomes 7/7, but the standing capacity
stays at 50 percent: an item existing and an item carrying the load are counted
separately. If full capacity is wanted after a loss, each remaining zone must carry the
entire load alone, and the rent climbs from 55 units to **95 units**. The third column
gives the real decision: the same guarantee spread across three zones drops to **75
units**. As the zone count grows, the share each zone carries shrinks with $1/(n-1)$, so
**three zones do the same job for 20 units less than two zones** — adding a failure
domain does not raise the capacity cost, it lowers it.

The second table shows where this decline stops. The extra cost falls by 20 units from
two to three zones, by 6.67 from three to four, and by 3.33 from four to five. The fourth
zone gains a third of what the third zone gained; **the point of diminishing returns is
three zones**, and by then the case for a fourth zone must rest on something other than
capacity cost.

The last line names where spreading falls short. In three of the seven items, the write
endpoint is single-zone; whichever zone holds that endpoint, its loss does not interrupt
read traffic but does interrupt **writes**. Spreading across zones keeps 7/7 items
standing while it does not keep the write path standing for 3/7 items.

#### When a Region Goes Down, and the Cost of a Synchronous Write

Zone separation divides the failure **inside** a region. The region itself — the shared
network entry, the shared control plane, the shared geography — is not divided.

- **CG5.** The round trip between zones in a region is 1.2 ms, between two regions 68 ms
  (fictional).
- **CG6.** The nightly batch job writes 480,000 read records, parallelism 8, window
  240 min; peak-day volume is three times as much. Base write time is 4.0 ms.

```js
// geography/region.mjs — region loss is independent of zone spread; cross-region latency ties to a
// synchronous write with a single number. Durations are DERIVED FROM THE MODEL, no clock is read.
import { ITEM, BASE, REGION_RENT, GLOBAL_RENT, scoped, measurePlan } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 2) => x.toFixed(n);
const RECORDS = 480_000, PARALLEL = 8, WINDOW = 240, PEAK = 3;  // fictional: nightly batch job
const WRITE = 4.0, ZONE_RTT = 1.2, REGION_RTT = 68.0;           // fictional ms

console.log("plan          | standing at zone loss   | standing at region loss   | single-region rent   | two-region rent  ");
console.log("--------------|-------------------------|---------------------------|----------------------|------------------");
for (const n of [1, 2, 3]) {
  const p = measurePlan(n);
  const rent = Number.isFinite(p.full) ? p.full : p.lean;
  console.log(`${["single-zone", "two-zone", "three-zone"][n - 1].padEnd(13)} | ` +
    `${s(`${p.standingItems}/${ITEM.length}`, 23)} | ${s(`${scoped("global").length}/${ITEM.length}`, 25)} | ` +
    `${s(y(rent), 20)} | ${s(y(2 * rent - GLOBAL_RENT), 17)}`);
}
console.log(`region loss is the same in all three plans: only the global item stands.`);
console.log();

console.log("write pattern         | round trip   | write time   | throughput (writes/s)   | batch job   | peak day    ");
console.log("----------------------|--------------|--------------|-------------------------|-------------|-------------");
for (const [name, rtt] of [["within-zone", 0], ["cross-zone", ZONE_RTT], ["cross-region", REGION_RTT]]) {
  const duration = WRITE + rtt;
  const min = (RECORDS * duration) / PARALLEL / 60_000;
  console.log(`${name.padEnd(21)} | ${s(y(rtt) + " ms", 12)} | ${s(y(duration) + " ms", 12)} | ` +
    `${s(y((1000 / duration) * PARALLEL), 23)} | ${s(y(min) + " min", 11)} | ${s(y(PEAK * min) + " min", 12)}`);
}
console.log(`window ${WINDOW} min; on peak day the cross-region synchronous write uses ` +
  `${y((100 * PEAK * RECORDS * (WRITE + REGION_RTT)) / PARALLEL / 60_000 / WINDOW, 1)}% of the window.`);
console.log();

console.log("scope    | items   | rent    | placement decision   | loss testable     | carried at exit  ");
console.log("---------|---------|---------|----------------------|-------------------|------------------");
for (const [k, rent] of [["zone", BASE], ["region", REGION_RENT], ["global", GLOBAL_RENT]]) {
  const tenant = k === "zone";
  console.log(`${k.padEnd(8)} | ${s(scoped(k).length, 7)} | ${s(y(rent), 7)} | ` +
    `${(tenant ? "tenant" : "provider").padEnd(20)} | ${(tenant ? "yes" : "no").padEnd(17)} | ` +
    `${tenant ? "yes" : "no"}`);
}
const handoff = ITEM.length - scoped("zone").length;
console.log(`placement decisions handed over: ${handoff}/${ITEM.length}; ` +
  `their rent is ${y(REGION_RENT + GLOBAL_RENT)} units and the extra capacity cost is 0.`);
```

```
plan          | standing at zone loss   | standing at region loss   | single-region rent   | two-region rent  
--------------|-------------------------|---------------------------|----------------------|------------------
single-zone   |                    0/13 |                      1/13 |                55.00 |            109.00
two-zone      |                    7/13 |                      1/13 |                95.00 |            189.00
three-zone    |                    7/13 |                      1/13 |                75.00 |            149.00
region loss is the same in all three plans: only the global item stands.

write pattern         | round trip   | write time   | throughput (writes/s)   | batch job   | peak day    
----------------------|--------------|--------------|-------------------------|-------------|-------------
within-zone           |      0.00 ms |      4.00 ms |                 2000.00 |    4.00 min |    12.00 min
cross-zone            |      1.20 ms |      5.20 ms |                 1538.46 |    5.20 min |    15.60 min
cross-region          |     68.00 ms |     72.00 ms |                  111.11 |   72.00 min |   216.00 min
window 240 min; on peak day the cross-region synchronous write uses 90.0% of the window.

scope    | items   | rent    | placement decision   | loss testable     | carried at exit  
---------|---------|---------|----------------------|-------------------|------------------
zone     |       7 |   40.00 | tenant               | yes               | yes
region   |       5 |   14.00 | provider             | no                | no
global   |       1 |    1.00 | provider             | no                | no
placement decisions handed over: 6/13; their rent is 15.00 units and the extra capacity cost is 0.
```

The second column of the first table is the same in all three rows: the item left
standing at a region loss is **1/13**, and the single standing item is name resolution.
The 20 or 40 units paid for zone separation does not move this column by a single item.
Keeping a full copy in a second region raises the rent in the three-zone plan from 75 to
**149 units** — roughly four times the cost of zone separation. Failure-domain separation
is layered, and each layer is paid for separately.

The second table shows the hidden part of that cost. The 1.2 ms cross-zone round trip
raises a synchronous write from 4.0 ms to 5.2 ms; the batch job stretches from 4.00
minutes to 5.20 minutes — a 30 percent increase that does not matter inside a 240-minute
window. A 68 ms cross-region round trip makes the same write **72.0 ms**, and write
throughput drops from 2000 to **111.11 writes/s**. The batch job climbs to 72 minutes; at
216 minutes on peak day it eats **90 percent** of the window and leaves 24 minutes to
spare. A cross-region synchronous write looks like a resilience decision, but the bill is
cut by the nightly batch job's window.

#### The Handed-Over Decision and What Carries at Exit

The third table is this lesson's leasing ledger. In **7 of the thirteen items**, the
placement decision sits with the tenant: how many copies go in which zone can be written
down, tested, and measured by dropping a zone. In the remaining **6 items**, the decision
has been handed over; their scope is region or global, and how they are distributed
across zones is invisible to the tenant.

The handoff pays back two items. First, the 15-unit rent already includes the cross-zone
distribution: the tenant pays **0 extra capacity** for these 6 items, whereas building
the same redundancy itself in a three-zone plan would cost 50 percent extra. Second, the
operating load of these 6 items never appears at all. The cost of the handoff is the
third column: the loss **cannot be tested**. Dropping a zone and measuring it is possible
only for the 7 items; for the 6 items, all that is left is the provider's word.

What carries at exit follows the same split. The placement plan of the 7 zone-scoped
items is a definition — how many copies, which separate failure domain, what capacity
after a loss — and this definition can be built again, unchanged, in another lease. The
distribution of the 6 region- and global-scoped items is never visible, so there is no
definition to carry either; on the other side, it must be remeasured, and in most cases
re-**bought**. Close to half of the rented geography does not come along on the way out.

#### Summary

- An availability zone is a placement unit with independent power, cooling, and network
  entry; a region is a low-latency cluster of such zones. The separation is a
  failure-domain question, not a geography question.
- In a single-zone placement, a zone loss leaves **0** of the 7 zone-scoped items standing
  and the standing capacity at 0 percent; in two and three zones, 7/7 items stay standing,
  but the write path does not stay standing for the 3 items whose write endpoint is
  single-zone.
- Rent for full capacity after a loss is 95 units at two zones, 75 at three, 68.33 at
  four: **three zones cost 20 units less than two**, and the point of diminishing returns
  is the third zone.
- The item left standing at a region loss is 1/13 in all three plans; a full copy in a
  second region raises the three-zone rent from 75 to 149 units.
- A cross-region synchronous write raises a write from 4.0 ms to 72.0 ms and the nightly
  batch job from 4 minutes to 72 minutes; at 216 minutes on peak day it uses 90 percent of
  the 240-minute window.
- The placement decision is handed to the provider for 6 of the 13 items: the extra
  capacity cost is 0, but the loss cannot be tested and no definition is left to carry at
  exit.

#### Next Step

This lesson read geography from the side of **failure**: wherever items are placed, what
a loss takes, how many are left standing, and what the separation costs in units. The
same geography has a second face that this lesson never measured: however items are
placed, how far they stay from the **user**. The 68 ms cross-region round trip appeared
here only as the cost of a write; the same number is also inside every response sent to
a distant subscriber, and it is there for that subscriber even with no failure at all.
The next lesson models the user distribution and the candidate location set, and counts
how the average distance falls as locations are added, the cost per location, and where
the decline stops; it also measures the work a location close to the user **cannot do**,
and the staleness of the response it returns.

### Edge Locations

Source: https://academia.sh/en/courses/cloud-fundamentals/edge-locations

Counting proximity to the user together with its cost: how average distance and latency fall as edge locations are added, where the gain per location stops, the share of work types the edge cannot serve, and the daily count of stale responses returned from the edge.

The previous lesson read geography from the side of failure: wherever an item is placed,
what a loss takes and what the separation costs in units. The same geography's second
face exists even with no failure at all. The farther a subscriber is from the region, the
longer the path every request travels; this path is paid every day, on every request, and
it is never caught by any health check.

An **edge location** is a placement point independent of the source region that sits
close to the user and serves a portion of requests from there. What is rented here is not
a machine but a **set of locations**: which of the provider's operated points to use is
chosen, not what runs at those points. This lesson counts the payoff of proximity and the
work it does not cover, together.

#### The Location Model

The setup below is a **model**. The fictional regional measurement network's subscribers
are distributed across a fictional plane; there is no real city, country, region, or
provider.

- **CG7.** The service geography is a fictional 1000×600-unit rectangle; the region sits
  at point (150, 500).
- **CG8.** 40 subscriber clusters are generated around five density hubs. The generator is
  self-written and the seed is visible as 20260802; every run gives the same numbers.
- **CG9.** Latency is linear in distance: 2.0 ms base + 0.075 ms per unit (fictional).
- **CG10.** An edge location's rent is 3.5 fictional `unit/hour`; the region's rent is
  outside this lesson's scope.
- **CG11.** Daily requests are 2,400,000, and the shares of the seven types are fixed.
- **CG12.** The hit rate in a warmed-up edge cache is 0.93; on the first day after a move,
  in a cold cache, 0.55.

```js
// edge/model.mjs — user distribution and location set is a MODEL. The fictional service
// geography is a 1000x600-unit rectangle; there is no real city, region, or provider.
export function generator(seed) {                // self-written linear congruential generator
  let s = seed >>> 0;
  return () => ((s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 4294967296);
}
export const SEED = 20260802;                    // visible seed; same numbers on every run

export const REGION = { name: "region", x: 150, y: 500 };
export const BASE_MS = 2.0, MS_PER_UNIT = 0.075;  // fictional: latency = base + distance * rate
export const LOCATION_RENT = 3.5;                 // fictional unit/hour, per edge location

export function subscribers() {                  // 5 density hubs, 8 clusters per hub
  const r = generator(SEED);
  const hub = Array.from({ length: 5 }, () => [r() * 1000, r() * 600]);
  const k = [];
  for (const [ox, oy] of hub)
    for (let i = 0; i < 8; i += 1)
      k.push({
        x: Math.min(1000, Math.max(0, ox + (r() + r() - 1) * 130)),
        y: Math.min(600, Math.max(0, oy + (r() + r() - 1) * 90)),
        weight: 1 + Math.floor(r() * 9),          // request share, 1..9
      });
  return k;
}

export function candidates() {                   // set of edge locations the provider offers
  const r = generator(SEED ^ 0x5f5f);
  return Array.from({ length: 14 }, (_, i) => ({ name: `k${i + 1}`, x: r() * 1000, y: r() * 600 }));
}

export const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
export const latency = (d) => BASE_MS + MS_PER_UNIT * d;

export function average(clusters, locations) {   // each cluster is served by the nearest location
  let weightSum = 0, sum = 0;
  for (const c of clusters) {
    sum += c.weight * Math.min(...locations.map((k) => distance(c, k)));
    weightSum += c.weight;
  }
  return sum / weightSum;
}

// request mix: types that can return from the edge and those that cannot (writes, reads that need consistency)
export const MIX = [
  ["static-shell", 0.28, "edge", 3600, 0.2],
  ["tariff-table", 0.10, "edge", 900, 2],
  ["subscriber-summary", 0.08, "edge", 60, 96],
  ["latest-read", 0.26, "consistent-read", 0, 0],
  ["read-upload", 0.18, "write", 0, 0],
  ["work-order-open", 0.06, "write", 0, 0],
  ["billing-close", 0.04, "write", 0, 0],
];
export const DAILY = 2_400_000, DAY_SEC = 86_400;
export const HIT_RATE = 0.93, COLD_HIT_RATE = 0.55;
```

#### As Locations Are Added

Locations are added greedily: at each step, the candidate that lowers the average
weighted distance the most is chosen. At each step the gain and the rent are written down
together.

```js
// edge/placement.mjs — as locations are added, average distance, latency, and cost per location move together
import { REGION, subscribers, candidates, average, latency, LOCATION_RENT, SEED } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 2) => x.toFixed(n);
const cluster = subscribers(), candidate = candidates();
const chosen = [REGION], remaining = [...candidate];
const weight = cluster.reduce((a, c) => a + c.weight, 0);

console.log(`seed ${SEED}: ${cluster.length} subscriber clusters, total weight ${weight}, ` +
  `${candidate.length} candidate edge locations (fictional geography).`);
console.log();
console.log("location   | added   | avg. distance   | avg. latency   | gain ms   | rent   | ms per unit     | total rent  ");
console.log("-----------|---------|-----------------|----------------|-----------|--------|-----------------|-------------");

let previousL = latency(average(cluster, chosen));
console.log(`${s(0, 10)} | ${"-".padEnd(7)} | ${s(y(average(cluster, chosen)), 15)} | ` +
  `${s(y(previousL), 14)} | ${s("-", 9)} | ${s("-", 6)} | ${s("-", 15)} | ${s(y(0), 12)}`);

for (let n = 1; n <= 8; n += 1) {
  let best = null;
  for (const a of remaining) {
    const d = average(cluster, [...chosen, a]);
    if (best === null || d < best.d) best = { a, d };
  }
  chosen.push(best.a);
  remaining.splice(remaining.indexOf(best.a), 1);
  const l = latency(best.d), gain = previousL - l;
  console.log(`${s(n, 10)} | ${best.a.name.padEnd(7)} | ${s(y(best.d), 15)} | ${s(y(l), 14)} | ` +
    `${s(y(gain), 9)} | ${s(y(LOCATION_RENT), 6)} | ${s(y(gain / LOCATION_RENT), 15)} | ` +
    `${s(y(n * LOCATION_RENT), 12)}`);
  previousL = l;
}
```

```
seed 20260802: 40 subscriber clusters, total weight 210, 14 candidate edge locations (fictional geography).

location   | added   | avg. distance   | avg. latency   | gain ms   | rent   | ms per unit     | total rent  
-----------|---------|-----------------|----------------|-----------|--------|-----------------|-------------
         0 | -       |          338.60 |          27.39 |         - |      - |               - |         0.00
         1 | k2      |          215.03 |          18.13 |      9.27 |   3.50 |            2.65 |         3.50
         2 | k12     |          172.38 |          14.93 |      3.20 |   3.50 |            0.91 |         7.00
         3 | k13     |          146.26 |          12.97 |      1.96 |   3.50 |            0.56 |        10.50
         4 | k3      |          137.53 |          12.31 |      0.65 |   3.50 |            0.19 |        14.00
         5 | k7      |          134.13 |          12.06 |      0.26 |   3.50 |            0.07 |        17.50
         6 | k14     |          131.84 |          11.89 |      0.17 |   3.50 |            0.05 |        21.00
         7 | k1      |          130.96 |          11.82 |      0.07 |   3.50 |            0.02 |        24.50
         8 | k9      |          130.41 |          11.78 |      0.04 |   3.50 |            0.01 |        28.00
```

The first location alone lowers the average latency from 27.39 ms to 18.13 ms: for 3.5
units of rent, 9.27 ms, that is **2.65 ms per unit**. The second location brings 3.20 ms
for the same rent, the third 1.96 ms. At the fourth, the gain per unit falls to 0.19 ms —
a third of what the third location gave, a fourteenth of what the first gave. **The point
of diminishing returns is the third location.**

The harshness of the limit shows in the last rows. Three locations bring latency down to
12.97 ms for 10.50 units of rent; eight locations bring it to 11.78 ms for 28.00 units.
Five extra locations and 17.50 units gain a total of 1.19 ms. The curve flattens because,
as the location count grows, each new candidate approaches the leftovers of a set that is
already mostly served; if the user distribution is clustered around hubs, once the first
three hubs are taken there is no remaining density left to pick up.

#### The Work the Edge Cannot Do

The table above assumes every request is served from the edge, and that assumption is
false. An edge location is a copy; it cannot accept a **write**, because the moment it
did, the record would diverge from the source. A **consistency-requiring read** cannot
return from the edge either: a subscriber asking for the reading they just uploaded
requires the source's most recent state.

```js
// edge/mix.mjs — work the edge cannot do, the blended real latency, and the staleness of an edge response
import { REGION, subscribers, candidates, average, latency, MIX, DAILY, DAY_SEC, HIT_RATE,
  COLD_HIT_RATE, LOCATION_RENT } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 2) => x.toFixed(n);
const cluster = subscribers(), candidate = candidates();
const chosen = [REGION];                            // the greedy choice's first three locations are rebuilt
for (let n = 0; n < 3; n += 1) {
  let e = null;
  for (const a of candidate)
    if (!chosen.includes(a)) {
      const d = average(cluster, [...chosen, a]);
      if (e === null || d < e.d) e = { a, d };
    }
  chosen.push(e.a);
}
const EDGE_MS = latency(average(cluster, chosen)), REGION_MS = latency(average(cluster, [REGION]));

console.log("type                 | share   | returns from edge   | TTL (s)   | daily change   | stale rate   | daily stale  ");
console.log("---------------------|---------|---------------------|-----------|----------------|--------------|--------------");
let staleTotal = 0, edgeShare = 0;
for (const [name, share, kind, ttl, change] of MIX) {
  const rate = kind === "edge" ? (change * (ttl / 2)) / DAY_SEC : 0;
  const stale = Math.round(share * DAILY * HIT_RATE * rate);
  staleTotal += stale; edgeShare += kind === "edge" ? share : 0;
  console.log(`${name.padEnd(20)} | ${s(y(share), 7)} | ${s(kind === "edge" ? "yes" : kind, 19)} | ` +
    `${s(ttl || "-", 9)} | ${s(change || "-", 14)} | ${s(rate ? `${y(100 * rate)}%` : "-", 12)} | ` +
    `${s(stale || "-", 13)}`);
}

const miss = EDGE_MS + REGION_MS;
const edgeBlend = HIT_RATE * EDGE_MS + (1 - HIT_RATE) * miss;
const blend = edgeShare * edgeBlend + (1 - edgeShare) * REGION_MS;
console.log();
console.log(`share returnable from the edge ${y(edgeShare)}; not returnable ${y(1 - edgeShare)} (write + consistent-read).`);
console.log(`latency: region ${y(REGION_MS)} ms, three-location edge ${y(EDGE_MS)} ms, ` +
  `on a miss ${y(miss)} ms, at hit rate ${HIT_RATE} the edge average is ${y(edgeBlend)} ms.`);
console.log(`blended real latency ${y(blend)} ms — a drop of ${y(REGION_MS - blend)} ms ` +
  `(${y((100 * (REGION_MS - blend)) / REGION_MS, 1)}%), whereas counting the edge alone would show ` +
  `${y((100 * (REGION_MS - EDGE_MS)) / REGION_MS, 1)}%.`);
console.log(`daily stale responses ${staleTotal.toLocaleString("en-US")} / ${DAILY.toLocaleString("en-US")} ` +
  `(${y((100 * staleTotal) / DAILY, 2)}%); the longest staleness is ${Math.max(...MIX.map((k) => k[3]))} s.`);

const warm = DAILY * (1 - edgeShare + edgeShare * (1 - HIT_RATE));
const cold = DAILY * (1 - edgeShare + edgeShare * (1 - COLD_HIT_RATE));
console.log(`requests reaching the origin: with a warmed-up cache ${Math.round(warm).toLocaleString("en-US")}, ` +
  `with a cold cache right after the move ${Math.round(cold).toLocaleString("en-US")} ` +
  `(+${y((100 * (cold - warm)) / warm, 1)}%); the rent for three locations is ${y(3 * LOCATION_RENT)} units.`);
```

```
type                 | share   | returns from edge   | TTL (s)   | daily change   | stale rate   | daily stale  
---------------------|---------|---------------------|-----------|----------------|--------------|--------------
static-shell         |    0.28 |                 yes |      3600 |            0.2 |        0.42% |          2604
tariff-table         |    0.10 |                 yes |       900 |              2 |        1.04% |          2325
subscriber-summary   |    0.08 |                 yes |        60 |             96 |        3.33% |          5952
latest-read          |    0.26 |     consistent-read |         - |              - |            - |             -
read-upload          |    0.18 |               write |         - |              - |            - |             -
work-order-open      |    0.06 |               write |         - |              - |            - |             -
billing-close        |    0.04 |               write |         - |              - |            - |             -

share returnable from the edge 0.46; not returnable 0.54 (write + consistent-read).
latency: region 27.39 ms, three-location edge 12.97 ms, on a miss 40.36 ms, at hit rate 0.93 the edge average is 14.89 ms.
blended real latency 21.64 ms — a drop of 5.75 ms (21.0%), whereas counting the edge alone would show 52.7%.
daily stale responses 10,881 / 2,400,000 (0.45%); the longest staleness is 3600 s.
requests reaching the origin: with a warmed-up cache 1,373,280, with a cold cache right after the move 1,792,800 (+30.5%); the rent for three locations is 10.50 units.
```

The share returnable from the edge is **0.46**; the remaining 0.54 is writes and
consistency-requiring reads, and it goes to the region no matter how many locations
exist. This is why the blended real latency stays at 21.64 ms and the drop is **21.0
percent** — a measurement that looked only at the edge column would have reported 52.7
percent. The gap between the two numbers is not a measurement error; it is the answer to
the question of which work can return from the edge.

The miss row carries a second warning. A request not found in the cache goes to the edge
and then on to the source, so it climbs to 40.36 ms — longer than going directly to the
region. An edge location does not speed up every request; it speeds up the ones it
**hits**, and slows down the ones it misses.

#### Staleness and What Carries at Exit

A response returned from the edge is, by definition, a copy, and a copy has an age. The
measure is the product of cache lifetime and how often the data changes: the subscriber
summary, with a 60-second lifetime, changes 96 times a day, and **3.33 percent** of its
requests return stale; the tariff table, at 900 seconds and 2 changes a day, is 1.04
percent; the static shell, at 3600 seconds but only 0.2 changes a day, is 0.42 percent.
The ranking is not the ranking of cache lifetime; it is the ranking of the **product** of
lifetime and change rate.

In total, **10,881 stale responses** return each day. This number, 0.45 percent of all
requests, is the number that has to be written down before a decision can be made about
whether it is acceptable; the longest staleness is 3600 seconds, which means a subscriber
whose tariff just changed can see a tariff up to an hour old.

The decision handed over here is three items: which locations are operated, which
location a request is routed to, and when an entry is evicted from the cache. What is
received in return is a 5.75 ms blended drop for 10.50 units of rent, and requests
reaching the origin falling from 2,400,000 to 1,373,280. What carries at exit is only the
**rules**: for the three types, the cache lifetime, the key, and the invalidation
definition can be written down and built again unchanged. Two things do not carry: the
provider's location set and the **warmed-up content**. With a cold cache, requests
reaching the origin on the first day climb to 1,792,800, a rise of **30.5 percent**; the
cost of the move, paid once, is exactly this difference.

#### Summary

- An edge location is renting a set of locations: which points to use is chosen, not what
  runs at those points.
- Average latency falls from 27.39 ms to 18.13 ms at the first location; the gain per unit
  drops from 2.65 ms to 0.56 ms at the third location and to 0.19 ms at the fourth — **the
  point of diminishing returns is the third location**.
- Three locations give 12.97 ms for 10.50 units, while eight locations give 11.78 ms for
  28.00 units: an extra 17.50 units gains a total of 1.19 ms.
- The share that cannot return from the edge is 0.54 (writes and consistency-requiring
  reads); the blended real latency drop is 21.0 percent, whereas counting the edge alone
  would show 52.7 percent. A missed request, at 40.36 ms, is slower than going directly to
  the region.
- Staleness is the product of lifetime and change rate: 3.33 percent for the subscriber
  summary, 1.04 percent for the tariff table, 0.42 percent for the static shell; 10,881
  stale responses a day and the longest staleness is 3600 seconds.
- The cache rules carry at exit; the location set and the warmed-up content do not: on a
  cold start, requests reaching the origin rise by 30.5 percent.

#### Next Step

Two lessons measured two axes separately, and the two pull against each other. Spreading
across three zones raised resilience and slowed the synchronous write. Three edge
locations lowered latency and produced 10,881 stale responses a day and 10.50 units of
rent. Every decision has a gain column and at least one loss column, but until now these
columns have been counted lesson by lesson and scattered. There is an arrangement that
holds decisions in a single table, scores each one against the same five measures, and
makes visible which measure conflicts with which. The next lesson defines those five
columns as types and scores a decision set against them, counting the tension across
columns: how many decisions that raise one column lower another, and which column no
decision improves.

### The Pillars of the Design Framework

Source: https://academia.sh/en/courses/cloud-fundamentals/design-framework-pillars

Scoring a decision set across five pillars and counting the tension between pillars: how many decisions raise one pillar while lowering another, which pillar pair is most tense, deriving the pillar no decision improves, and comparing the pillar profile of handed-over decisions against what remains and the share that does not carry at exit.

The first two lessons measured geography on two separate axes, and both ended in the same
shape: a decision raised one number and lowered another. Spreading across three zones
raised the item left standing and hurt the rent and the synchronous write time. Three
edge locations lowered latency and produced more than ten thousand stale responses a day.
Each of these two observations is, on its own, an anecdote; to turn into a decision
ledger they have to be written down with the same measures and in the same form.

The design frameworks used to evaluate rented infrastructure do exactly this. They score
a decision not as good or bad but **separately, pillar by pillar**. This lesson uses five
pillars — operations, security, reliability, performance, and cost — because those five
are the common core of the widely used frameworks; the number of pillars varies from
framework to framework, and some add a sixth such as sustainability. The framework's name
does not appear in this lesson; what is measured is not a name but how the pillars
constrain one another. A tension matrix built the same way was set up for quality
attributes in a previous course; the method is the same, but the numbers there do not
carry over here.

#### Five Pillars and the Decision Set

- **CG13.** The pillars are called by their type name: operations, security, reliability,
  performance, cost.
- **CG14.** The decision set consists of 12 decisions made in this course and the previous
  two lessons; all are geography, resilience, or leasing decisions.
- **CG15.** The scores are an **assumption** between -2 and +2, not a measured value; what
  is meaningful is the ranking and the sign, not the absolute size.
- **CG16.** A decision counts as a **handoff** if implementing it requires renting a
  service the provider operates.
- **CG17.** The exit class has three values: `definition` (can be written down and built
  again unchanged in another lease), `partial`, `none`.
- **CG18.** Decisions are scored independently of one another. In real sets they affect
  each other; this is a known limit of the model.

```js
// framework/model.mjs — decision set and pillar scores is a MODEL. Pillars are called by their type
// name; scores are this lesson's assumption, not measured values. There is no real product or catalog.
export const PILLAR = ["operations", "security", "reliability", "performance", "cost"];

// name, [operations, security, reliability, performance, cost] (-2..+2), handoff?, exit class
export const DECISION = [
  ["three-zone-spread", [-1, 0, 2, 0, -1], false, "definition"],
  ["full-copy-in-second-region", [-2, 0, 2, 1, -2], false, "definition"],
  ["cross-region-synchronous-write", [-1, 0, 2, -2, -1], false, "definition"],
  ["three-edge-locations", [-1, 1, 1, 2, -1], true, "partial"],
  ["subscriber-summary-cache", [0, 0, 1, 2, 0], true, "definition"],
  ["managed-relational-database", [2, 1, 1, -1, -2], true, "partial"],
  ["function-runtime", [2, 0, 1, -1, -1], true, "none"],
  ["provider-key-management", [1, 2, 0, -1, -1], true, "none"],
  ["per-environment-separate-lease", [-2, 2, 1, 0, -1], true, "definition"],
  ["automatic-patching-handoff", [2, 2, -1, 0, 0], true, "none"],
  ["single-write-endpoint", [1, 0, -2, 1, 0], false, "definition"],
  ["raw-read-in-object-store", [1, 0, 1, -1, -1], true, "partial"],
];

export const score = (k, s) => k[1][PILLAR.indexOf(s)];
export const raising = (s) => DECISION.filter((k) => score(k, s) > 0);
export const lowering = (s) => DECISION.filter((k) => score(k, s) < 0);
export const total = (s) => DECISION.reduce((a, k) => a + score(k, s), 0);

// tension: if decision i raises pillar i while lowering pillar j, the cell increases by one
export function tension(i, j) {
  return DECISION.filter((k) => score(k, i) > 0 && score(k, j) < 0).length;
}
export const tense = (k) => k[1].some((p) => p > 0) && k[1].some((p) => p < 0);
```

#### Tension Between Pillars

```js
// framework/score.mjs — pillar totals and the tension matrix between pillars
import { PILLAR, DECISION, score, raising, lowering, total, tension, tense } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
console.log(`${DECISION.length} decisions, ${PILLAR.length} pillars; scores are in -2..+2 (assumption).`);
console.log();
console.log("decision                          | ops   | sec   | rel   | prf   | cst   | net   | tense  ");
console.log("----------------------------------|-------|-------|-------|-------|-------|-------|--------");
for (const k of DECISION) {
  const net = k[1].reduce((a, b) => a + b, 0);
  console.log(`${k[0].padEnd(33)} |${k[1].map((p) => s(p, 5)).join(" |")} | ${s(net, 5)} | ` +
    `${tense(k) ? "yes" : "no"}`);
}

console.log();
console.log("pillar        | total   | raising decisions   | lowering decisions   | highest   | lowest  ");
console.log("--------------|---------|---------------------|----------------------|-----------|---------");
for (const t of PILLAR) {
  const p = DECISION.map((k) => score(k, t));
  console.log(`${t.padEnd(13)} | ${s(total(t), 7)} | ${s(raising(t).length, 19)} | ` +
    `${s(lowering(t).length, 20)} | ${s(Math.max(...p), 9)} | ${s(Math.min(...p), 8)}`);
}

console.log();
console.log("raising \\ lowering   | operations   | security   | reliability   | performance   | cost    | row  ");
console.log("---------------------|--------------|------------|---------------|---------------|---------|------");
for (const i of PILLAR) {
  const h = PILLAR.map((j) => (i === j ? "-" : String(tension(i, j))));
  const row = PILLAR.filter((j) => j !== i).reduce((a, j) => a + tension(i, j), 0);
  console.log(`${i.padEnd(20)} | ${s(h[0], 12)} | ${s(h[1], 10)} | ${s(h[2], 13)} | ` +
    `${s(h[3], 13)} | ${s(h[4], 7)} | ${s(row, 5)}`);
}

const neverRaised = PILLAR.filter((t) => raising(t).length === 0);
const pair = [];
for (const i of PILLAR) for (const j of PILLAR) if (i !== j) pair.push([i, j, tension(i, j)]);
pair.sort((a, b) => b[2] - a[2]);
console.log();
console.log(`tense decisions ${DECISION.filter(tense).length}/${DECISION.length}; ` +
  `decisions that lower no pillar ${DECISION.filter((k) => !k[1].some((p) => p < 0)).length}.`);
console.log(`pillar no decision improves: ${neverRaised.join(", ") || "none"}.`);
console.log(`most tense pair: ${pair[0][0]} rises while ${pair[0][1]} falls — ${pair[0][2]} decisions.`);
```

```
12 decisions, 5 pillars; scores are in -2..+2 (assumption).

decision                          | ops   | sec   | rel   | prf   | cst   | net   | tense  
----------------------------------|-------|-------|-------|-------|-------|-------|--------
three-zone-spread                 |   -1 |    0 |    2 |    0 |   -1 |     0 | yes
full-copy-in-second-region        |   -2 |    0 |    2 |    1 |   -2 |    -1 | yes
cross-region-synchronous-write    |   -1 |    0 |    2 |   -2 |   -1 |    -2 | yes
three-edge-locations              |   -1 |    1 |    1 |    2 |   -1 |     2 | yes
subscriber-summary-cache          |    0 |    0 |    1 |    2 |    0 |     3 | no
managed-relational-database       |    2 |    1 |    1 |   -1 |   -2 |     1 | yes
function-runtime                  |    2 |    0 |    1 |   -1 |   -1 |     1 | yes
provider-key-management           |    1 |    2 |    0 |   -1 |   -1 |     1 | yes
per-environment-separate-lease    |   -2 |    2 |    1 |    0 |   -1 |     0 | yes
automatic-patching-handoff        |    2 |    2 |   -1 |    0 |    0 |     3 | yes
single-write-endpoint             |    1 |    0 |   -2 |    1 |    0 |     0 | yes
raw-read-in-object-store          |    1 |    0 |    1 |   -1 |   -1 |     0 | yes

pillar        | total   | raising decisions   | lowering decisions   | highest   | lowest  
--------------|---------|---------------------|----------------------|-----------|---------
operations    |       2 |                   6 |                    5 |         2 |       -2
security      |       8 |                   5 |                    0 |         2 |        0
reliability   |       9 |                   9 |                    2 |         2 |       -2
performance   |       0 |                   4 |                    5 |         2 |       -2
cost          |     -11 |                   0 |                    9 |         0 |       -2

raising \ lowering   | operations   | security   | reliability   | performance   | cost    | row  
---------------------|--------------|------------|---------------|---------------|---------|------
operations           |            - |          0 |             2 |             4 |       4 |    10
security             |            2 |          - |             1 |             2 |       4 |     9
reliability          |            5 |          0 |             - |             4 |       8 |    17
performance          |            2 |          0 |             1 |             - |       2 |     5
cost                 |            0 |          0 |             0 |             0 |       - |     0

tense decisions 11/12; decisions that lower no pillar 1.
pillar no decision improves: cost.
most tense pair: reliability rises while cost falls — 8 decisions.
```

The last column of the first table is this lesson's first number: **11 of the 12
decisions are tense**, meaning each raises at least one pillar while lowering at least
one other. The only decision that lowers no pillar is the subscriber summary cache, and
even that one came with a cost in the second lesson — stale responses. If the pillar
scoring did not see the cost, that does not mean there is no cost; it means **it fell
outside the measure set**.

The second table summarizes the pillars on their own. Reliability is raised by 9
decisions and, at +9, is the highest-total pillar. Cost, in contrast, is raised by **0
decisions**, lowered by 9, and totals -11. This does not show the decisions were badly
chosen; it is a consequence of what the set is made of. This lesson gathered geography
and resilience decisions, and none of them raise cost — the decisions that raise cost
come from a different kind and have not been measured yet. Security showing 0 lowering
decisions reads the same way: this set contains no decision that breaks security, not
that security cannot be broken.

The third table is the real measure. The row counts the decisions that raise one pillar;
the column counts the measure they lower. The reliability row, at 17, is by far the most
tense row: the decisions that raise reliability lower cost in 8 decisions, operations in
5, and performance in 4. **The most tense pair is reliability and cost, and 8 decisions
carry this pair.** The cost row being entirely zero is the matrix form of the same
finding as the second table: since no decision raises cost, there is no cell where cost
lowers another pillar either.

Where the matrix earns its keep is not a ranking but a list of **which pair needs to be
settled explicitly**. Because the reliability–cost cell is filled with 8, if that pair is
not discussed once and pinned to an upper bound, eight separate decisions each make their
own trade-off, unaware of the other seven. The reliability–security cell, by contrast, is
0: none of the nine decisions that raise reliability lower security, so there is no need
to write a separate trade-off rule for that pair. Empty cells carry as much information
as full ones.

#### The Pillar Profile of the Handed-Over Decision

The tension matrix split decisions across pillars; the second cut splits them by **who
owns them**. A decision is handed over if implementing it requires renting a service the
provider operates.

```js
// framework/handoff.mjs — splitting the pillar profile between handed-over and retained-own decisions
import { PILLAR, DECISION, score } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x) => x.toFixed(2);
const EXIT = { definition: 2, partial: 1, none: 0 };
const group = { handoff: DECISION.filter((k) => k[2]), own: DECISION.filter((k) => !k[2]) };

console.log("group     | decisions   | operations   | security   | reliability   | performance   | cost    | exit score  ");
console.log("----------|-------------|--------------|------------|---------------|---------------|---------|-------------");
for (const [name, g] of Object.entries(group)) {
  const avg = PILLAR.map((t) => g.reduce((a, k) => a + score(k, t), 0) / g.length);
  const exitScore = g.reduce((a, k) => a + EXIT[k[3]], 0) / (2 * g.length);
  console.log(`${name.padEnd(9)} | ${s(g.length, 11)} | ${s(y(avg[0]), 12)} | ${s(y(avg[1]), 10)} | ` +
    `${s(y(avg[2]), 13)} | ${s(y(avg[3]), 13)} | ${s(y(avg[4]), 7)} | ${s(`${y(100 * exitScore)}%`, 12)}`);
}

console.log();
console.log("group     | definition at exit   | partial   | none   | decisions not carried  ");
console.log("----------|----------------------|-----------|--------|------------------------");
for (const [name, g] of Object.entries(group)) {
  const count = (t) => g.filter((k) => k[3] === t).length;
  console.log(`${name.padEnd(9)} | ${s(count("definition"), 20)} | ${s(count("partial"), 9)} | ` +
    `${s(count("none"), 6)} | ${s(`${count("partial") + count("none")}/${g.length}`, 23)}`);
}

const handoffOpsSec = group.handoff.reduce((a, k) => a + score(k, "security") + score(k, "operations"), 0);
const ownOpsSec = group.own.reduce((a, k) => a + score(k, "security") + score(k, "operations"), 0);
console.log();
console.log(`handed-over ${group.handoff.length} decisions total ${handoffOpsSec} in operations + security, ` +
  `retained-own ${group.own.length} decisions ${ownOpsSec}.`);
console.log(`of the handed-over ${group.handoff.length} decisions, those that cannot carry at exit as a ` +
  `definition: ${group.handoff.filter((k) => k[3] !== "definition").length}; of the retained-own ` +
  `${group.own.length}, ${group.own.filter((k) => k[3] !== "definition").length}.`);
```

```
group     | decisions   | operations   | security   | reliability   | performance   | cost    | exit score  
----------|-------------|--------------|------------|---------------|---------------|---------|-------------
handoff   |           8 |         0.63 |       1.00 |          0.63 |          0.00 |   -0.88 |       43.75%
own       |           4 |        -0.75 |       0.00 |          1.00 |          0.00 |   -1.00 |      100.00%

group     | definition at exit   | partial   | none   | decisions not carried  
----------|----------------------|-----------|--------|------------------------
handoff   |                    2 |         3 |      3 |                     6/8
own       |                    4 |         0 |      0 |                     0/4

handed-over 8 decisions total 13 in operations + security, retained-own 4 decisions -3.
of the handed-over 8 decisions, those that cannot carry at exit as a definition: 6; of the retained-own 4, 0.
```

The difference between handed-over and retained-own decisions concentrates in two
pillars. In operations, handed-over decisions average +0.63, retained-own decisions
**-0.75**; in security, +1.00 versus 0.00. Their sums, 13 against -3, mean the payoff of
the lease is paid in these two pillars. In reliability, the ranking reverses —
retained-own decisions average +1.00, handed-over decisions +0.63. The decisions that
raise reliability the most are decisions that cannot be handed to anyone: spreading
across zones, keeping a copy in a second region, making the write synchronous.

The last table gives the exit column, and this lesson's sharpest contrast is here. **All
4** of the 4 retained-own decisions carry at exit as a definition: how many zones, which
region, which write pattern — every one is a sentence that can be written down and built
again unchanged in another lease. Of the 8 handed-over decisions, only **2** carry; 6
carry partially or not at all. The exit score is 100 percent against **43.75 percent**.
Leasing raises the operations and security pillars while pulling the exit column below
half, and this column appears in none of the five above.

#### Summary

- The framework scores a decision not as good or bad but pillar by pillar; the five used
  in this lesson are operations, security, reliability, performance, and cost. What is
  measured is not a name but how the pillars constrain one another.
- **11 of the 12 decisions are tense**; there is exactly one decision that lowers no
  pillar, and even its cost fell outside the pillars.
- **0** decisions raise cost, 9 lower it; the pillar no decision improves is cost. This is
  a consequence of the set being made of geography and resilience decisions.
- The most tense pair is reliability and cost: 8 decisions raise reliability while
  lowering cost. The reliability row, at 17, is the most tense row.
- The 8 handed-over decisions average +0.63 and +1.00 in operations and security, the 4
  retained-own decisions -0.75 and 0.00; in reliability the ranking reverses (+1.00
  against +0.63).
- At exit, 4/4 of the retained-own decisions carry as a definition, only 2/8 of the
  handed-over ones: the exit score is 100 percent against 43.75 percent.

#### Next Step

This lesson counted cost as a pillar, and that pillar turned up a strange property: none
of the twelve decisions raise it. The reason is not that the decisions were poor, but the
composition of the set — geography and resilience decisions are the side that pays cost,
not the side that earns it. The decisions that raise the cost pillar come from elsewhere,
and not one of them has been measured yet: **under which terms** the same capacity is
rented. The same machine, the same hour, the same work; but capacity carries one price if
it can be requested and released instantly, another if it is tied to a long-term
commitment, and a third if the provider can reclaim it. The next lesson runs three models
against the same load profile and counts, together, the total cost, the work re-run
because reclaimed capacity was lost, and the unused commitment; it shows that as the load
profile changes, the winning model changes too.

### Pricing Models

Source: https://academia.sh/en/courses/cloud-fundamentals/pricing-models

Running on-demand, reserved, and spot capacity against the same load profile: total cost, work re-run because of reclaimed capacity, unused commitment, the gap between list price and effective unit price, and the liability left when exiting mid-window.

The previous lesson counted cost as a pillar and found that none of the twelve decisions
raised it. The reason was not that the decisions were poor, but that all of them were
about **what** is rented. This lesson's question is different: the same capacity, the
same hour, the same work — **under which terms** is it rented.

There are three terms. **On-demand capacity** is taken the moment it is requested and
stops being paid for the moment it is released; no decision is handed over, and the unit
price is highest. **Reserved capacity** gives a discount in exchange for a commitment to
pay for a fixed amount across the window; the decision handed over is the decision to
**shrink capacity**. **Spot capacity** gives the largest discount, but the provider can
reclaim it; the decision handed over is the decision of **when the work gets cut**. The
measurement below runs these three terms under the same load.

#### Three Models and Load Profiles

- **CG19.** Prices are **fictional** and in fictional `unit/cu-hour`: on-demand 1.00,
  reserved 0.62, spot 0.28. No real currency or price list is used.
- **CG20.** The window is 168 hours. The unpredictable profile is generated with a
  self-written generator; the seed is visible as 20260804 and every run gives the same
  sequence.
- **CG21.** The reserved commitment R is the average demand, rounded, and is paid across
  the window whether it is used or not; demand exceeding R is served on-demand.
- **CG22.** Spot capacity's reclaim probability depends on load:
  $p = 0.02 + 0.40 \cdot (demand / 120)$, at most 0.45.
- **CG23.** Only **deferrable** work runs on spot capacity. The deferrable share is
  0.45 / 0.60 / 0.10 depending on the profile, because the three profiles are produced by
  different kinds of work.
- **CG24.** The work reclaimed capacity was doing is lost and re-runs on on-demand
  capacity; the re-run multiplier is 2.5 (the loss of intermediate state and re-queuing).

```js
// price/model.mjs — pricing is a MODEL. Prices are FICTIONAL and in fictional
// `unit/cu-hour`; no real currency, provider, or price list is used.
export function generator(seed) {                  // self-written generator, seed visible
  let s = seed >>> 0;
  return () => ((s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 4294967296);
}
export const SEED = 20260804, HOURS = 168;          // one-week window

export const PRICE = { onDemand: 1.00, reserved: 0.62, spot: 0.28 };  // fictional unit/cu-hour
export const RERUN = 2.5;                           // re-run multiplier for lost work
export const MARKET_PEAK = 120;                     // fictional: scale point of reclaim probability
export const reclaim = (d) => Math.min(0.45, 0.02 + 0.40 * (d / MARKET_PEAK));

// profile: demand series + deferrable share (differs because the profile's source work differs)
export function profiles() {
  const r = generator(SEED);
  const flat = Array.from({ length: HOURS }, () => 40);
  const spiky = Array.from({ length: HOURS }, (_, t) => (t % 24 >= 2 && t % 24 <= 4 ? 120 : 20));
  const unpredictable = Array.from({ length: HOURS }, () =>
    (r() < 0.6 ? Math.round(r() * 6) : 55 + Math.round(r() * 85)));
  return { flat: [flat, 0.45], spiky: [spiky, 0.60], unpredictable: [unpredictable, 0.10] };
}

export function run(model, demand, deferrable) {
  const R = Math.round(demand.reduce((a, b) => a + b, 0) / demand.length);  // commitment = avg. demand
  let cost = 0, loss = 0, unusedCommitment = 0, rerunCost = 0;
  for (const d of demand) {
    if (model === "on-demand") cost += d * PRICE.onDemand;
    else if (model === "reserved") {
      cost += R * PRICE.reserved + Math.max(0, d - R) * PRICE.onDemand;
      unusedCommitment += Math.max(0, R - d);
    } else {
      const q = d * deferrable, p = reclaim(d), k = q * p;
      cost += q * (1 - p) * PRICE.spot + k * RERUN * PRICE.onDemand
        + (d - q) * PRICE.onDemand;
      loss += k; rerunCost += k * RERUN * PRICE.onDemand;
    }
  }
  return { model, cost, loss, unusedCommitment, rerunCost, R };
}
export const MODEL = ["on-demand", "reserved", "spot"];
```

#### Three Runs Against the Same Profile

```js
// price/run.mjs — three models run against the same load profile; total cost, work lost
// on spot, and unused commitment on reserved are counted together
import { profiles, run, MODEL, HOURS, SEED, PRICE } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 2) => x.toFixed(n);
const P = profiles();

console.log(`seed ${SEED}, window ${HOURS} hours; prices are FICTIONAL unit/cu-hour: ` +
  `on-demand ${y(PRICE.onDemand)}, reserved ${y(PRICE.reserved)}, spot ${y(PRICE.spot)}.`);
console.log();
console.log("profile         | total demand   | avg    | lowest   | highest   | deferrable share   | commitment R  ");
console.log("----------------|----------------|--------|----------|-----------|--------------------|---------------");
for (const [name, [t, e]] of Object.entries(P)) {
  const top = t.reduce((a, b) => a + b, 0);
  console.log(`${name.padEnd(15)} | ${s(top, 14)} | ${s(y(top / HOURS, 1), 6)} | ${s(Math.min(...t), 8)} | ` +
    `${s(Math.max(...t), 9)} | ${s(y(e), 18)} | ${s(run("reserved", t, e).R, 14)}`);
}

console.log();
console.log("profile         | model        | total cost     | lost work     | rerun cost    | unused commitment  ");
console.log("----------------|--------------|----------------|---------------|---------------|--------------------");
const winner = {};
for (const [name, [t, e]] of Object.entries(P)) {
  let best = null;
  for (const m of MODEL) {
    const r = run(m, t, e);
    if (best === null || r.cost < best.cost) best = r;
    console.log(`${name.padEnd(15)} | ${m.padEnd(12)} | ${s(y(r.cost), 14)} | ` +
      `${s(r.loss ? y(r.loss, 1) : "-", 13)} | ${s(r.rerunCost ? y(r.rerunCost) : "-", 13)} | ` +
      `${s(r.unusedCommitment ? y(r.unusedCommitment, 1) : "-", 18)}`);
  }
  winner[name] = best;
}

console.log();
console.log("profile         | winner       | gap to 2nd    | gap to 2nd %   | gap to priciest %  ");
console.log("----------------|--------------|---------------|----------------|--------------------");
for (const [name, [t, e]] of Object.entries(P)) {
  const ranked = MODEL.map((m) => run(m, t, e)).sort((a, b) => a.cost - b.cost);
  const [a, b, c] = ranked;
  console.log(`${name.padEnd(15)} | ${a.model.padEnd(12)} | ${s(y(b.cost - a.cost), 13)} | ` +
    `${s(`${y((100 * (b.cost - a.cost)) / a.cost, 1)}%`, 14)} | ` +
    `${s(`${y((100 * (c.cost - a.cost)) / a.cost, 1)}%`, 18)}`);
}
console.log(`winning model across the three profiles: ${Object.values(winner).map((k) => k.model).join(", ")}.`);
```

```
seed 20260804, window 168 hours; prices are FICTIONAL unit/cu-hour: on-demand 1.00, reserved 0.62, spot 0.28.

profile         | total demand   | avg    | lowest   | highest   | deferrable share   | commitment R  
----------------|----------------|--------|----------|-----------|--------------------|---------------
flat            |           6720 |   40.0 |       40 |        40 |               0.45 |             40
spiky           |           5460 |   32.5 |       20 |       120 |               0.60 |             33
unpredictable   |           7430 |   44.2 |        0 |       138 |               0.10 |             44

profile         | model        | total cost     | lost work     | rerun cost    | unused commitment  
----------------|--------------|----------------|---------------|---------------|--------------------
flat            | on-demand    |        6720.00 |             - |             - |                  -
flat            | reserved     |        4166.40 |             - |             - |                  -
flat            | spot         |        5572.09 |         463.7 |       1159.20 |                  -
spiky           | on-demand    |        5460.00 |             - |             - |                  -
spiky           | reserved     |        5264.28 |             - |             - |             1911.0
spiky           | spot         |        4850.46 |         787.9 |       1969.80 |                  -
unpredictable   | on-demand    |        7430.00 |             - |             - |                  -
unpredictable   | reserved     |        8528.04 |             - |             - |             3907.0
unpredictable   | spot         |        7475.12 |         261.3 |        653.25 |                  -

profile         | winner       | gap to 2nd    | gap to 2nd %   | gap to priciest %  
----------------|--------------|---------------|----------------|--------------------
flat            | reserved     |       1405.69 |          33.7% |              61.3%
spiky           | spot         |        413.82 |           8.5% |              12.6%
unpredictable   | on-demand    |         45.12 |           0.6% |              14.8%
winning model across the three profiles: reserved, spot, on-demand.
```

The last line is this lesson's finding: **three different models win across the three
profiles.** In the flat profile, reserved capacity wins at 4,166.40 units, while the same
load costs 6,720.00 on demand — 61.3 percent more; because the commitment is used in full,
unused commitment is zero. In
the spiky profile, reserved capacity loses, because the commitment is set to the average
and 87.5 percent of demand falls below that average: **1,911 cu-hours** are paid for but
unused. In the unpredictable profile, no handoff pays off at all, and on-demand capacity,
at the highest list price, wins at 7,430.00 units.

Spot capacity's two hidden items sit in separate columns in the middle table. In the flat
profile, 463.7 cu-hours of work is lost along with the reclaimed capacity, and re-running
it costs **1,159.20 units** — more than a fifth of that model's total cost. In the spiky
profile, the loss is 787.9 cu-hours and the re-run cost is 1,969.80 units; even though
this profile is the winner, 40 percent of its cost is the price of reclaimed work. **Spot
capacity's real price is not in the list; it is in the reclaim rate.**

The spot row in the unpredictable profile shows this item's second face: the lost work,
at 261.3 cu-hours, is the lowest of the three profiles, yet the model still loses
(7,475.12 against 7,430.00). The reason is not the loss but the ceiling on the gain — in
that profile the deferrable share is 0.10, so the discount can only reach a tenth of
demand. **A discount is a discount only up to the share it can touch.**

The third table gives the size of the gaps. In the flat profile, the winner beats the
runner-up by 33.7 percent: model choice is a real decision here. In the unpredictable
profile, the gap falls to 0.6 percent — the cost of the three models is nearly identical,
and the choice is decided not by cost but by **risk**.

#### Effective Unit Price and the Cost of Exit

List price does not say how much a lease actually costs. What needs to be measured is the
**effective unit price**: total cost divided by the total demand served.

```js
// price/exit.mjs — effective unit price, the decision handed over, and the cost of exiting mid-window
import { profiles, run, MODEL, HOURS, PRICE } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 3) => x.toFixed(n);
const P = profiles(), EXIT_HOUR = 84;               // the lease is exited mid-window
const HANDOFF = { "on-demand": 0, reserved: 1, spot: 2 };
const LIST = { "on-demand": PRICE.onDemand, reserved: PRICE.reserved, spot: PRICE.spot };

console.log("profile         | model        | list   | effective unit   | vs. list     | handoff          ");
console.log("----------------|--------------|--------|------------------|--------------|------------------");
for (const [name, [t, e]] of Object.entries(P)) {
  const top = t.reduce((a, b) => a + b, 0);
  for (const m of MODEL) {
    const effective = run(m, t, e).cost / top;
    console.log(`${name.padEnd(15)} | ${m.padEnd(12)} | ${s(y(LIST[m], 2), 6)} | ${s(y(effective), 16)} | ` +
      `${s(`x${y(effective / LIST[m], 2)}`, 12)} | ${s(HANDOFF[m], 17)}`);
  }
}

console.log();
console.log(`remaining liability when exiting in hour ${EXIT_HOUR} of the window (fictional units):`);
console.log("profile         | on-demand    | spot      | reserved (remaining commitment)   | share of reserved gain   ");
console.log("----------------|--------------|-----------|-----------------------------------|--------------------------");
for (const [name, [t, e]] of Object.entries(P)) {
  const r = run("reserved", t, e), full = run("on-demand", t, e);
  const remaining = (HOURS - EXIT_HOUR) * r.R * PRICE.reserved;
  const gain = full.cost - r.cost;
  console.log(`${name.padEnd(15)} | ${s(0, 12)} | ${s(0, 9)} | ${s(y(remaining, 2), 33)} | ` +
    `${s(gain > 0 ? `${y((100 * remaining) / gain, 1)}%` : "no gain", 25)}`);
}
```

```
profile         | model        | list   | effective unit   | vs. list     | handoff          
----------------|--------------|--------|------------------|--------------|------------------
flat            | on-demand    |   1.00 |            1.000 |        x1.00 |                 0
flat            | reserved     |   0.62 |            0.620 |        x1.00 |                 1
flat            | spot         |   0.28 |            0.829 |        x2.96 |                 2
spiky           | on-demand    |   1.00 |            1.000 |        x1.00 |                 0
spiky           | reserved     |   0.62 |            0.964 |        x1.56 |                 1
spiky           | spot         |   0.28 |            0.888 |        x3.17 |                 2
unpredictable   | on-demand    |   1.00 |            1.000 |        x1.00 |                 0
unpredictable   | reserved     |   0.62 |            1.148 |        x1.85 |                 1
unpredictable   | spot         |   0.28 |            1.006 |        x3.59 |                 2

remaining liability when exiting in hour 84 of the window (fictional units):
profile         | on-demand    | spot      | reserved (remaining commitment)   | share of reserved gain   
----------------|--------------|-----------|-----------------------------------|--------------------------
flat            |            0 |         0 |                           2083.20 |                     81.6%
spiky           |            0 |         0 |                           1718.64 |                    878.1%
unpredictable   |            0 |         0 |                           2291.52 |                   no gain
```

Spot capacity's list price is 0.28, 72 percent below on-demand. Its effective unit
price, though, is 0.829 in the flat profile, 0.888 in the spiky profile, and **1.006** in
the unpredictable profile — 2.96, 3.17, and 3.59 times the list price, respectively. In
the unpredictable profile, the model that looks cheapest actually ends up **more
expensive** than the one that looks priciest. The same pattern plays out more mildly for
reserved capacity: at a list of 0.62, the effective price is 0.964 in the spiky profile
and 1.148 in the unpredictable one.

The handoff column ranks these three: 0 for on-demand, 1 for reserved, 2 for spot. The
order matches the list discount exactly — the discount is the payoff for the decision
handed over, and all three models' effective price depends on how many times that
handed-over decision worked against them.

The last table counts the exit. For on-demand and spot capacity, the remaining liability
from exiting mid-window is **zero**. For reserved capacity, in the flat profile, 2,083.20
units remain — that is **81.6 percent** of the total gain that same model produced in
that profile. In the spiky profile, the remaining liability is **878.1 percent** of the
gain: exiting in the middle of a commitment wipes out the commitment's entire gain eight
times over. The exit cost of a lease, here, is not an estimate; it is a number the
contract writes down directly.

#### Summary

- The three capacity models rank by the number of decisions handed over: on-demand 0,
  reserved 1 (shrinking capacity), spot 2 (when the work gets cut); the list discount
  follows this order.
- When the same three models run against three load profiles, **three different models
  win**: reserved in the flat profile, spot in the spiky profile, on-demand in the
  unpredictable profile. The runner-up costs 33.7, 8.5 and 0.6 percent more than the
  winner, respectively. There is no single correct model.
- Spot capacity's hidden item is reclaimed work: 463.7 cu-hours lost and 1,159.20 units of
  re-run cost in the flat profile; 787.9 cu-hours and 1,969.80 units in the spiky profile.
- Reserved capacity's hidden item is unused commitment: 1,911 cu-hours are paid for and
  unused in the spiky profile, 3,907 in the unpredictable profile.
- List price and effective unit price are separate numbers: spot's list is 0.28, but its
  effective price ranges from 0.829 to 1.006 (2.96–3.59 times the list) and, in the
  unpredictable profile, exceeds on-demand.
- At exit, the remaining liability of on-demand and spot capacity is zero; for reserved
  capacity, exiting mid-window wipes out 81.6 percent of the gain in the flat profile and
  878.1 percent in the spiky profile.

#### Next Step

Four lessons measured the geography and the price of rented infrastructure:
failure-domain separation, proximity to the user, tension between pillars, and the terms
under which capacity is taken. All four assumed one thing — that the work to be rented is
**already rented**. But the fictional regional measurement network runs today in its own
room, and it has to get here from somewhere. The same service can be moved as it stands,
its individual pieces can be swapped for rented counterparts, or it can be rewritten from
scratch; the three do not give the same result. The next lesson compares these three
migration paths in terms of the **decision handed over**: how many decisions pass to the
provider on each path, how many items get rewritten, how many waves the migration takes
to complete, and, most importantly, how much the exit cost counted across these four
lessons grows on each path.

### Cloud Migration Strategies

Source: https://academia.sh/en/courses/cloud-fundamentals/cloud-migration-strategies

Comparing rehost, replatform, and refactor across the same ten-item set: the decision handed over, migration days, the provider-specific item that makes exit harder, the wave count and how long the two systems run together, the annual load of the item left behind, and the cost of a dual system that never ends.

The previous lesson measured the terms under which capacity is rented and found that
three different models won across three load profiles. All of those calculations were
made under one silent assumption: that the work to be moved is **already rented**. But
the fictional regional measurement network runs today in its own room.

Three paths are measured here. **Rehost** moves the item as-is onto a rented machine; the
code does not change. **Replatform** replaces some of the item's parts with managed
counterparts; the interface layer is rewritten. **Refactor** rewrites the item from
scratch around rented building blocks. The common migration frameworks also count
repurchasing an item as a bought service, retiring it, or leaving it where it is; this
lesson stays with the three because it compares three depths of moving the same item.
The wave mechanics of a staged rollout and the pattern of wrapping the old system piece
by piece were measured in earlier courses; what
is measured here is not the wave itself but the annual cost of the case where the waves
**never finish**.

#### The Item Set and Three Strategies

- **CG25.** The item set is 10 services; each item's size (1–3), annual operating days,
  and annual change count are fictional.
- **CG26.** Strategy profiles are fixed per item and written in the model file: the
  decisions handed over are 3, 7, and 11, respectively.
- **CG27.** The decision horizon is 5 years; an item whose five-year net gain is not
  above zero is not moved.
- **CG28.** The team processes 20 migration days per wave; a wave takes 30 calendar days.
- **CG29.** As long as the two systems run together, 1.5 extra operating days accrue per
  month. If any item is left behind, this cost never ends.
- **CG30.** On the mixed path, each item gets the strategy that gives the highest
  five-year net.

```js
// migration/model.mjs — migration is a MODEL: the fictional measurement network's ten items and three
// strategy profiles. There is no real migration tool, provider, or calendar; every number is fictional.
// name, size (1-3), annual operating days, annual change count
export const ITEM = [
  ["read-collector", 3, 22, 30], ["validator", 2, 14, 18], ["billing", 3, 18, 12],
  ["work-order-service", 2, 10, 9], ["subscriber-portal", 2, 12, 24], ["nightly-batch-job", 1, 8, 4],
  ["read-store", 3, 20, 3], ["tariff-store", 1, 6, 2], ["report-endpoint", 1, 3, 1],
  ["identity-directory", 1, 5, 2],
];

// handoff: decisions handed over per item; drop: share dropped from operating load; delivery: days gained
// per change; days: migration days per unit size; specific: number of provider-specific items per item
export const STRATEGY = {
  rehost: { handoff: 3, drop: 0.15, delivery: 0.05, days: 2, specific: 1, rewrite: "unchanged" },
  replatform: { handoff: 7, drop: 0.55, delivery: 0.20, days: 9, specific: 3, rewrite: "partial" },
  refactor: { handoff: 11, drop: 0.80, delivery: 0.70, days: 34, specific: 5, rewrite: "full" },
};
export const NAME = Object.keys(STRATEGY);
export const HORIZON = 5, WAVE_CAP = 20, WAVE_DAYS = 30, DUAL_MONTH = 1.5;  // fictional

export const migrationDays = (k, s) => STRATEGY[s].days * k[1];
export const gain = (k, s) => k[2] * STRATEGY[s].drop + k[3] * STRATEGY[s].delivery;
export const net = (k, s) => gain(k, s) * HORIZON - migrationDays(k, s);
export const best = (k) => NAME.reduce((a, s) => (net(k, s) > net(k, a) ? s : a), NAME[0]);

// One path: one strategy per item. An item whose net gain stays below zero is not moved.
export function run(pick) {
  const moved = ITEM.filter((k) => net(k, pick(k)) > 0);
  const left = ITEM.filter((k) => net(k, pick(k)) <= 0);
  const sum = (f) => moved.reduce((a, k) => a + f(k), 0);
  const days = sum((k) => migrationDays(k, pick(k)));
  const waves = Math.ceil(days / WAVE_CAP), together = waves * WAVE_DAYS;
  const transition = (together / 30) * DUAL_MONTH;
  const persistent = left.length ? DUAL_MONTH * 12 : 0;
  const annual = sum((k) => gain(k, pick(k))) - persistent;
  return {
    moved: moved.length, left: left.length, days, waves, together, transition, persistent, annual,
    handoff: sum((k) => STRATEGY[pick(k)].handoff),
    specific: sum((k) => STRATEGY[pick(k)].specific),
    remainingLoad: sum((k) => k[2] * (1 - STRATEGY[pick(k)].drop)) + left.reduce((a, k) => a + k[2], 0),
    leftLoad: left.reduce((a, k) => a + k[2], 0),
    net5: annual * HORIZON - days - transition,
  };
}
```

#### Three Strategies on the Same Set

```js
// migration/strategy.mjs — three strategies on the same item set: decisions handed over,
// item rewritten, migration days, and the provider-specific item that makes exit harder
import { ITEM, STRATEGY, NAME, HORIZON, run, net, best } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 1) => x.toFixed(n);
const SHORT = { rehost: "rehost", replatform: "replatform",
  refactor: "refactor" };
const R = Object.fromEntries(NAME.map((a) => [a, run(() => a)]));
R.mixed = run(best);
const PATH = [...NAME, "mixed"];

console.log(`${ITEM.length} items, decision horizon ${HORIZON} years; every number is FICTIONAL ` +
  `and in "operating days" and "migration days".`);
console.log();
console.log("measure                           | rehost      | replatform   | refactor   | mixed  ");
console.log("----------------------------------|-------------|--------------|------------|--------");
for (const [name, f] of [
  ["items moved", (r) => `${r.moved}/${ITEM.length}`],
  ["decisions handed over", (r) => r.handoff],
  ["item rewritten", (r, a) => (a === "mixed" ? "mixed" : STRATEGY[a].rewrite)],
  ["migration days (total)", (r) => r.days],
  ["provider-specific items (exit)", (r) => r.specific],
  ["annual gain (operating days)", (r) => y(r.annual)],
  ["remaining annual operating load", (r) => y(r.remainingLoad)]])
  console.log(`${name.padEnd(33)} | ${s(f(R[PATH[0]], PATH[0]), 11)} | ${s(f(R[PATH[1]], PATH[1]), 12)} | ` +
    `${s(f(R[PATH[2]], PATH[2]), 10)} | ${s(f(R.mixed, "mixed"), 7)}`);

console.log();
console.log(`${HORIZON}-year net gain per item (operating days; the highest is chosen)`);
console.log("item                 | size     | load   | change   | rehost     | replatform   | refactor   | choice      ");
console.log("---------------------|----------|--------|----------|------------|--------------|------------|-------------");
for (const k of ITEM) {
  const n = NAME.map((a) => net(k, a));
  console.log(`${k[0].padEnd(20)} | ${s(k[1], 8)} | ${s(k[2], 6)} | ${s(k[3], 8)} | ` +
    `${s(y(n[0]), 10)} | ${s(y(n[1]), 12)} | ${s(y(n[2]), 10)} | ${SHORT[best(k)]}`);
}
const count = (a) => ITEM.filter((k) => best(k) === a).length;
console.log(`choice distribution: ` + NAME.map((a) => `${a} ${count(a)}`).join(", ") +
  `; all three strategies win on at least one item.`);
```

```
10 items, decision horizon 5 years; every number is FICTIONAL and in "operating days" and "migration days".

measure                           | rehost      | replatform   | refactor   | mixed  
----------------------------------|-------------|--------------|------------|--------
items moved                       |       10/10 |        10/10 |       6/10 |   10/10
decisions handed over             |          30 |           70 |         66 |      78
item rewritten                    |   unchanged |      partial |       full |   mixed
migration days (total)            |          38 |          171 |        442 |     339
provider-specific items (exit)    |          10 |           30 |         30 |      34
annual gain (operating days)      |        22.9 |         85.9 |      117.1 |   132.6
remaining annual operating load   |       100.3 |         53.1 |       50.8 |    42.3

5-year net gain per item (operating days; the highest is chosen)
item                 | size     | load   | change   | rehost     | replatform   | refactor   | choice      
---------------------|----------|--------|----------|------------|--------------|------------|-------------
read-collector       |        3 |     22 |       30 |       18.0 |         63.5 |       91.0 | refactor
validator            |        2 |     14 |       18 |       11.0 |         38.5 |       51.0 | refactor
billing              |        3 |     18 |       12 |       10.5 |         34.5 |       12.0 | replatform
work-order-service   |        2 |     10 |        9 |        5.8 |         18.5 |        3.5 | replatform
subscriber-portal    |        2 |     12 |       24 |       11.0 |         39.0 |       64.0 | refactor
nightly-batch-job    |        1 |      8 |        4 |        5.0 |         17.0 |       12.0 | replatform
read-store           |        3 |     20 |        3 |        9.8 |         31.0 |      -11.5 | replatform
tariff-store         |        1 |      6 |        2 |        3.0 |          9.5 |       -3.0 | replatform
report-endpoint      |        1 |      3 |        1 |        0.5 |          0.3 |      -18.5 | rehost
identity-directory   |        1 |      5 |        2 |        2.3 |          6.8 |       -7.0 | replatform
choice distribution: rehost 1, replatform 6, refactor 3; all three strategies win on at least one item.
```

The decisions handed over are 30, 70, and 66. Refactor's 66 is not a saving but a
**shortfall**: it hands over 11 decisions per item, yet can only move six of the ten
items. Migration days climb from 38 to 171 and to 442, and the provider-specific items
that make exit harder rise from 10 to 30. Rehost gains the least (22.9 operating days
annually) but is the path that harms exit the least.

The second table shows where this gain comes from. What the three items refactor wins
have in common is not load but **change** count: 30, 18, and 24. The read store, which
changes only 3 times a year, is the second-heaviest item by load (20 annual days), yet
nets **-11.5** under refactor; the payoff of rewriting comes not from operating load but
from **cheaper changes**. The report endpoint is the extreme case: size 1, load 3, change
1, and **-18.5** under refactor — **on a small, static service, refactor never pays back
within five years**; the best choice there is rehost, at 0.5. The choice distribution is
1, 6, and 3: all three strategies win on at least one item.

#### Staging and the Cost of Not Moving

```js
// migration/wave.mjs — staging and the cost of not moving: service per wave, how long the
// two systems run together, the annual load left behind, and the dual system that never ends
import { NAME, WAVE_CAP, WAVE_DAYS, DUAL_MONTH, run, best } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const y = (x, n = 1) => x.toFixed(n);
const R = { ...Object.fromEntries(NAME.map((a) => [a, run(() => a)])), mixed: run(best) };
const PATH = [["rehost", NAME[0]], ["replatform", NAME[1]], ["refactor", NAME[2]], ["mixed", "mixed"]];

console.log(`wave capacity ${WAVE_CAP} migration days, wave duration ${WAVE_DAYS} calendar days; ` +
  `as long as the two systems run together, ${DUAL_MONTH} extra operating days accrue per month (fictional).`);
console.log();
console.log("path         | migration days   | waves   | items/wave   | together   | left   | dual system   | 5-year net  ");
console.log("-------------|------------------|---------|--------------|------------|--------|---------------|-------------");
for (const [name, v] of PATH) {
  const r = R[v];
  console.log(`${name.padEnd(12)} | ${s(r.days, 16)} | ${s(r.waves, 7)} | ${s(y(r.moved / r.waves), 12)} | ` +
    `${s(`${r.together} d`, 10)} | ${s(r.left, 6)} | ` +
    `${s(r.persistent ? `${y(r.persistent)}/yr` : "ends", 13)} | ${s(y(r.net5), 12)}`);
}
const m = R[NAME[2]];
console.log(`transition cost (one-time): ` + PATH.map(([a, v]) => `${a} ${y(R[v].transition)}`).join(", ") + ".");
console.log(`refactor leaves ${m.left} items behind; their annual load of ${y(m.leftLoad)} days ` +
  `and the dual system at ${y(m.persistent)} days/yr never end.`);
```

```
wave capacity 20 migration days, wave duration 30 calendar days; as long as the two systems run together, 1.5 extra operating days accrue per month (fictional).

path         | migration days   | waves   | items/wave   | together   | left   | dual system   | 5-year net  
-------------|------------------|---------|--------------|------------|--------|---------------|-------------
rehost       |               38 |       2 |          5.0 |       60 d |      0 |          ends |         73.7
replatform   |              171 |       9 |          1.1 |      270 d |      0 |          ends |        245.0
refactor     |              442 |      23 |          0.3 |      690 d |      4 |       18.0/yr |        109.0
mixed        |              339 |      17 |          0.6 |      510 d |      0 |          ends |        298.3
transition cost (one-time): rehost 3.0, replatform 13.5, refactor 34.5, mixed 25.5.
refactor leaves 4 items behind; their annual load of 34.0 days and the dual system at 18.0 days/yr never end.
```

Staging converts the wave count into a calendar. Rehost finishes in 2 waves and moves
**5.0 services per wave**; replatform falls to 1.1, and refactor to **0.3 services** —
most waves cannot even finish a single service. The two systems run together for 60,
270, and **690 days**, and the transition cost that accrues over that time is 3.0, 13.5,
and 34.5 operating days.

The real item is in the last two columns. For rehost and replatform, the dual system
**ends**; for refactor it does not, because four items are never moved. Those four
items' annual operating load of **34.0 days** stays as it is, and on top of it, the dual
system adds **18.0 days/year** permanently. Not moving is not a saving; it is an
unending subscription: the path that is second in annual gain (117.1) drops to third in
the five-year net, at **109.0**, while replatform posts 245.0.

The mixed path beats them all: 339 migration days, 17 waves, **0** left behind, and a
five-year net of **298.3** — the cost of applying a single strategy to the whole set is
53.3 operating days relative to the best uniform path.

#### Summary

- The three strategies hand over 30, 70, and 66 decisions across the same ten items;
  migration days are 38, 171, and 442, and provider-specific items are 10, 30, and 30.
  Refactor is second in annual gain (117.1 days) but third in the five-year net (109.0
  against 245.0).
- The payoff of rewriting comes from change count, not operating load: the three items it
  wins have annual changes of 30, 18, and 24, while the read store, which changes only 3
  times a year, nets -11.5. It posts -18.5 on the small, static report endpoint; the best
  choice there is rehost (0.5).
- Staging moves 5.0, 1.1, and 0.3 services per wave; the two systems run together for 60,
  270, and 690 days, and the transition cost is 3.0, 13.5, and 34.5 operating days.
- Not moving is an unending cost: the 34.0-day annual load of the four items left behind
  does not shrink, and the dual system does not end at 18.0 days/year. The mixed path,
  choosing per item, leaves nothing behind and posts 298.3 in the five-year net.

#### Course Wrap-Up

| Lesson | Decision Handed Over | Received in Exchange | Cannot Carry at Exit |
|---|---|---|---|
| What Is Cloud Computing | 3 decisions (purchase, quantity, placement) | wait 30.0 → 1.0 days | 4 of 8 items |
| Service Models | 5 / 23 / 25 of 26 | touch 91 → 82 / 19 / 4 | embedded 2 / 8 / 17 decisions |
| Deployment Models | 16/24 of the checks | elasticity window 21.0 → 1.0 days | 16 check items, 6 boundary bindings |
| Comparison with On-Premises | ownership, 8-unit upfront purchase | hidden share 64% → 13% | 250 fu exit fee |
| Shared Responsibility Model | 6 / 16 / 20 of 24 | left with the customer 18 → 8 → 4 | negotiable 6 / 16 / 20 items |
| Regions and Availability Zones | placement of 6 of 13 items | 0 extra capacity cost | distribution definition of 6 items |
| Edge Locations | 3 decisions (location, routing, eviction) | latency drop 21.0% | warmed-up cache, +30.5% load |
| The Pillars of the Design Framework | 8 of 12 decisions | operations +0.63, security +1.00 | 6 of 8 handoffs (43.75%) |
| Pricing Models | 0 / 1 / 2 decisions | on-demand 61.3% more expensive in the flat profile | remaining commitment 2,083.20 units |
| Cloud Migration Strategies | 30 / 70 / 66 decisions | annual 22.9 / 85.9 / 117.1 days | provider-specific 10 / 30 / 30 |

The course's rule is the same across all ten rows: **leasing is handing over a decision;
every decision handed over carries a gain and an exit cost.** Without the middle column,
the left column is a list of losses; without the right column, the middle column is a
sales pitch.

The last column, across ten rows, comes down to four families. A **decision embedded in
the service** recurs across four lessons (service models, shared responsibility,
pillars, migration strategies): the item moves, the decision that configures it does
not. A **distribution operated by the provider and never visible** appears in two
lessons (deployment models, regions): something that cannot be tested has no definition
to carry either. **State accumulated over time** appears in two lessons (performance
history, warmed-up cache). The **remaining liability the contract writes down** appears
in two lessons (exit fee, remaining commitment) and, of the four, is the only family
that is visible up front. The remaining three are discovered only at the moment of exit,
unless they are measured beforehand.

The decision to lease was made, its depth and pattern were chosen, the responsibility
boundary was drawn, its geography and price were set, and the path of moving it was
measured. Yet the **most concrete thing** being rented was never taken up in these ten
lessons: the machine itself, and the network that connects those machines to one
another. Capacity was always counted as an abstract "unit," the network always as a
"latency." **M23/K02 Compute and Networking in the Cloud** opens up those two
abstractions: what the difference between rented machine types is measured by, what
items a network is built from, and how many decisions the path between two machines
carries.

