Lesson 02 / 12
Platform as a Product
Measuring the internal customer and the feedback loop: the same twenty-four requests are run under an on-demand regime and a product-managed regime; adoption, the count of capabilities that go unused, the work spent on them, the time between a request and its fulfillment, and the gap the product approach opens in product teams are all counted.
Published Updated
In this lesson
- Runs the same request set under two operating models and compares wait time, adoption, and unfulfilled-request counts
- Counts capabilities that go unused and the work spent on them separately in both regimes
- Measures the product approach's discovery work and the gap it opens in product teams in the same unit as the platform's gain
Contents
The previous lesson built the platform as a cost calculation: eighteen items, a handoff cost, the remainder left with the team, and the threshold where it pays for itself. Every line of that calculation assumed which item to hand off was already known. In practice, a platform team does not hold a list of items; it holds requests, and a request is not the same thing as a need. Two teams state the same need through two separate requests; one team stops wanting what it asked for by the time it arrives.
This lesson runs the same request set under two operating models. The on-demand regime takes each request as it arrives and fulfills them in order. The product-managed regime treats teams as an internal customer: it groups requests by the need behind them, checks how many teams are waiting on a given need, and builds a need once; the cost is discovery work. What gets counted: adoption, capabilities that go unused, the work spent on them, the time between a request and its fulfillment, and the gap the product approach opens in product teams. The through-line is the regional measurement network and its five teams; both the network and the teams are fictional.
Two Operating Models
PA7 — five teams open twenty-four requests in the first twelve quarters; fourteen distinct needs sit behind those requests. The run lasts sixteen quarters, because the backlog can only be seen draining once the request flow stops. PA8 — the platform’s capacity is 26 person-days per quarter, and it is the same in every run. The reason: the two regimes can only be compared under the same capacity.
PA9 — the on-demand regime processes requests in arrival order and builds every request that lands on the same need as a separate capability. PA10 — the product-managed regime spends the first two quarters on discovery alone, sets aside 4 person-days of capacity per quarter for discovery afterward, builds a need once, and moves the one with the most waiting teams to the front. The reason: what sets the product approach apart is that it takes priority away from request order and ties it to the breadth of the need, and that information does not come free.
PA11 — if the fulfillment arrives more than four quarters after the request, the team has already built its own workaround and never uses that capability. The reason: waiting is not only delay; past a certain length, what gets delivered has no one left to receive it. A third run, the product regime without discovery, is added to measure the cost of discovery work by itself.
// request.mjs — the same request set run under two operating models (model) import { writeFileSync } from "node:fs"; // PA7 — five teams open 24 requests in the first twelve quarters, behind which sit 14 distinct needs; // the run lasts sixteen quarters so the backlog has time to drain. Numbers are fictional. const NEED = { "environment request": 28, "secret distribution": 22, "pipeline template": 18, "evidence archive": 16, "workload template": 24, "network rule template": 14, "scale setting": 12, "state record handoff": 34, "version ledger": 10, "dashboard wiring": 20, "log pipe": 26, "alert template": 12, "image scan gate": 18, "privilege template": 30, }; const REQUEST = [ [1, "meter", "environment request"], [1, "billing", "state record handoff"], [2, "validation", "pipeline template"], [3, "field", "environment request"], [3, "reporting", "dashboard wiring"], [4, "billing", "secret distribution"], [4, "meter", "network rule template"], [5, "validation", "scale setting"], [5, "field", "log pipe"], [6, "reporting", "pipeline template"], [6, "billing", "version ledger"], [7, "meter", "dashboard wiring"], [7, "validation", "workload template"], [8, "field", "alert template"], [8, "billing", "evidence archive"], [9, "reporting", "log pipe"], [9, "meter", "image scan gate"], [10, "validation", "privilege template"], [10, "field", "secret distribution"], [11, "billing", "pipeline template"], [11, "reporting", "environment request"], [12, "meter", "alert template"], [12, "validation", "state record handoff"], [12, "field", "privilege template"], ].map(([quarter, team, need]) => ({ quarter, team, need })); // PA8 capacity is 26 person-days/quarter in both runs. PA9 on-demand regime: arrival order, every // request its own capability. PA10 product regime: first two quarters discovery only, then 4 // person-days/quarter discovery, one need built once, sorted by waiting team count. PA11 patience is four quarters. const CAPACITY = 26, DISCOVERY = 4, WINDOW = 16, PATIENCE = 4; function run(name, product, hasDiscovery = product) { const requests = REQUEST.map((r) => ({ ...r, done: null })); const open = [], built = new Map(); let build = 0, discovery = 0; for (let c = 1; c <= WINDOW; c++) { for (const r of requests.filter((x) => x.quarter === c)) { if (product && built.has(r.need)) { r.done = c; continue; } const match = product && open.find((a) => a.need === r.need); if (match) { match.team.add(r.team); continue; } open.push({ need: r.need, team: new Set([r.team]), remaining: NEED[r.need] }); } let cap = CAPACITY; if (hasDiscovery) { const d = c <= 2 ? CAPACITY : DISCOVERY; discovery += d; cap -= d; } if (product) open.sort((a, b) => b.team.size - a.team.size || a.remaining - b.remaining); while (cap > 0 && open.length) { const job = open[0], spent = Math.min(cap, job.remaining); job.remaining -= spent; cap -= spent; build += spent; if (job.remaining > 0) break; open.shift(); if (product) built.set(job.need, c); for (const r of requests) if (r.need === job.need && r.quarter <= c && r.done === null && (product || job.team.has(r.team))) { r.done = c; if (!product) break; } } } for (const r of requests) { r.wait = r.done === null ? null : r.done - r.quarter; r.adopted = r.done !== null && r.wait <= PATIENCE; } return { name, product, requests, built: [...built], build, discovery, capabilities: product ? built.size : requests.filter((r) => r.done !== null).length }; } const RUNS = [run("on-demand", false), run("product-managed", true), run("product, no discovery", true, false)]; writeFileSync("runs.json", JSON.stringify({ NEED, RUNS, PATIENCE })); const avg = (v) => (v.length ? (v.reduce((a, b) => a + b, 0) / v.length).toFixed(1) : "-"); console.log("regime".padEnd(24) + "capabilities".padStart(13) + "build".padStart(7) + "discovery".padStart(11) + "fulfilled".padStart(12) + "unfulfilled".padStart(15) + "avg. wait".padStart(14) + "adopting".padStart(12)); for (const d of RUNS) { const b = d.requests.filter((r) => r.done !== null); console.log(d.name.padEnd(24) + `${d.capabilities}`.padStart(13) + `${d.build}`.padStart(7) + `${d.discovery}`.padStart(11) + `${b.length}`.padStart(12) + `${d.requests.length - b.length}`.padStart(15) + avg(b.map((r) => r.wait)).padStart(14) + `${d.requests.filter((r) => r.adopted).length}`.padStart(12)); } console.log("\n" + "team".padEnd(13) + "requests".padStart(9) + RUNS.map((d) => d.name.padStart(24) + "adopting".padStart(12)).join("")); for (const e of [...new Set(REQUEST.map((r) => r.team))]) { console.log(e.padEnd(13) + `${REQUEST.filter((r) => r.team === e).length}`.padStart(9) + RUNS.map((d) => { const x = d.requests.filter((r) => r.team === e); return avg(x.filter((r) => r.done !== null).map((r) => r.wait)).padStart(24) + `${x.filter((r) => r.adopted).length}`.padStart(12); }).join("")); }
regime capabilities build discovery fulfilled unfulfilled avg. wait adopting on-demand 20 416 0 20 4 3.2 16 product-managed 14 284 108 24 0 2.3 20 product, no discovery 14 284 0 24 0 0.7 24 team requests on-demand adopting product-managed adopting product, no discovery adopting meter 5 3.0 3 1.0 5 0.4 5 billing 5 3.2 4 4.4 3 1.8 5 validation 5 3.3 3 3.4 4 0.2 5 field 5 3.5 3 1.4 4 0.8 5 reporting 4 3.0 3 1.3 4 0.0 4
Wait and Adoption
The first table puts the two regimes side by side. The on-demand regime builds twenty
capabilities and spends 416 person-days; four requests never even make it into the queue
across sixteen quarters. The product regime meets the same twenty-four requests with fourteen
capabilities and 284 person-days of build work — four more requests fulfilled, with one
hundred thirty-two fewer person-days. The difference is not in build speed, it is in
duplication: both pipeline template and environment request get built three times over,
because every request is its own capability.
The wait numbers soften this. Average wait is 3.2 quarters in the on-demand regime and 2.3 quarters in the product regime; it drops to 0.7 quarters in the discovery-free run. The 1.6-quarter gap between them is the price of discovery work: the whole of the first two quarters, and four person-days of every quarter after, never go to build.
The second table shows who the product regime takes from. meter‘s wait drops from 3.0 to 1.0,
field‘s from 3.5 to 1.4. billing moves the other way: its wait rises from 3.2 to 4.4
quarters and the number of requests it adopts falls from four to three. The cause is the
prioritization rule — version ledger and evidence archive are needs only billing asks for,
and they fall to the back of a queue sorted by waiting-team count. **The product approach moves
the majority’s need to the front while pushing the minority’s need to the back; this is not a
side effect, it is the rule itself.**
Dead Capabilities and the Cost of the Product Approach
PA12 — the product regime’s discovery work is subtracted from platform capacity; on top of that, every request costs the requesting team 2 person-days of consultation, and every quarter costs every team 1 person-day of feedback. This second item is the gap opened in product teams, and it is never written to the platform’s own ledger. Forty person-days count as one person-quarter. The reason: the cost of the product approach does not accumulate in the platform team, it accumulates in the teams it measures; the two sides cannot be added without converting to the same unit.
// adoption.mjs — adoption distribution, dead capabilities, and the cost of the product approach import { readFileSync } from "node:fs"; const { NEED, RUNS, PATIENCE } = JSON.parse(readFileSync("runs.json", "utf8")); const TEAM = [...new Set(RUNS[0].requests.map((r) => r.team))]; // The adopting teams of a capability: in the on-demand regime it belongs to the single team that opened the request; // in the product regime every team that wanted the same need shares one capability. function capabilitiesOf(d, patience = PATIENCE) { const adopts = (r) => r.done !== null && r.done - r.quarter <= patience; if (!d.product) return d.requests.filter((r) => r.done !== null) .map((r) => ({ need: r.need, work: NEED[r.need], adopters: adopts(r) ? 1 : 0 })); return d.built.map(([need]) => ({ need, work: NEED[need], adopters: d.requests.filter((r) => r.need === need && adopts(r)).length })); } console.log("regime".padEnd(24) + "capabilities".padStart(13) + "duplicated build".padStart(18) + "dead capabilities".padStart(19) + "work on dead capabilities".padStart(27) + "team-capability pairs".padStart(23) + "per capability".padStart(16)); for (const d of RUNS) { const Y = capabilitiesOf(d), dead = Y.filter((y) => y.adopters === 0); const distinctCost = [...new Set(Y.map((y) => y.need))].reduce((t, i) => t + NEED[i], 0); const pairs = Y.reduce((t, y) => t + y.adopters, 0); console.log(d.name.padEnd(24) + `${Y.length}`.padStart(13) + `${d.build - distinctCost}`.padStart(18) + `${dead.length}`.padStart(19) + `${dead.reduce((t, y) => t + y.work, 0)}`.padStart(27) + `${pairs}`.padStart(23) + (pairs / Y.length).toFixed(2).padStart(16)); } const count = (need) => new Set(RUNS[0].requests.filter((r) => r.need === need).map((r) => r.team)).size; for (const d of RUNS.slice(0, 2)) { const dead = [...new Set(capabilitiesOf(d).filter((y) => y.adopters === 0).map((y) => y.need))]; console.log(`${d.name}: capabilities never used — ` + dead.map((need) => `${need} (${NEED[need]} person-days, ${count(need)} team${count(need) === 1 ? "" : "s"} wanted it)`).join("; ")); } // PA12 — the product regime's discovery work is subtracted from platform capacity; on top of it, every request costs // the requesting team 2 person-days of consultation and every quarter costs every team 1 person-day of feedback. This // second item is the gap opened in product teams. 40 person-days count as one person-quarter. const PERSON_QUARTER = 40, WINDOW = 16; const consultation = RUNS[0].requests.length * 2, feedback = TEAM.length * WINDOW; const gap = consultation + feedback, discovery = RUNS[1].discovery; console.log(`\ncost of the product regime — platform-side discovery ${discovery} person-days ` + `(${(discovery / PERSON_QUARTER).toFixed(1)} person-quarters); product-team consultation ${consultation} + ` + `feedback ${feedback} = ${gap} person-days (${(gap / PERSON_QUARTER).toFixed(1)} person-quarters)`); console.log(`platform's gain in build work: ${RUNS[0].build} - ${RUNS[1].build} = ` + `${RUNS[0].build - RUNS[1].build} person-days; gap written to product teams ${gap} person-days ` + `(${(100 * gap / (RUNS[0].build - RUNS[1].build)).toFixed(0)}% of the gain)`); // The length of the feedback loop is the one lever: the patience window is scanned. console.log("\n" + "patience (quarters)".padEnd(21) + RUNS.map((d) => d.name.padStart(24)).join("") + " (adopted requests / work on dead capabilities)"); for (let s = 2; s <= 6; s++) console.log(`${s}`.padEnd(21) + RUNS.map((d) => { const Y = capabilitiesOf(d, s); const b = d.requests.filter((r) => r.done !== null && r.done - r.quarter <= s).length; return `${b} / ${Y.filter((y) => y.adopters === 0).reduce((t, y) => t + y.work, 0)}` .padStart(24); }).join(""));
regime capabilities duplicated build dead capabilities work on dead capabilities team-capability pairs per capability on-demand 20 132 4 88 16 0.80 product-managed 14 0 1 24 20 1.43 product, no discovery 14 0 0 0 24 1.71 on-demand: capabilities never used — image scan gate (18 person-days, 1 team wanted it); privilege template (30 person-days, 2 teams wanted it); secret distribution (22 person-days, 2 teams wanted it); pipeline template (18 person-days, 3 teams wanted it) product-managed: capabilities never used — workload template (24 person-days, 1 team wanted it) cost of the product regime — platform-side discovery 108 person-days (2.7 person-quarters); product-team consultation 48 + feedback 80 = 128 person-days (3.2 person-quarters) platform's gain in build work: 416 - 284 = 132 person-days; gap written to product teams 128 person-days (97% of the gain) patience (quarters) on-demand product-managed product, no discovery (adopted requests / work on dead capabilities) 2 7 / 254 15 / 40 21 / 0 3 13 / 154 18 / 24 22 / 0 4 16 / 88 20 / 24 24 / 0 5 19 / 22 21 / 24 24 / 0 6 20 / 0 22 / 24 24 / 0
The adoption column carries the real difference. In the on-demand regime, a capability is used by 0.80 teams on average — below one, because four of the capabilities are never used at all. The same figure is 1.43 in the product regime and 1.71 in the discovery-free run. Twenty capabilities produce sixteen team-capability pairs, while fourteen capabilities produce twenty: fewer capabilities, more use.
The row for capabilities never used tells a different story in each regime. In the on-demand
regime, four capabilities are dead and 88 person-days went into them; one of those, pipeline template, is a need three teams asked for — it mattered when the request was made and arrived
too late by the time it was fulfilled. In the product regime there is exactly one dead
capability: workload template, 24 person-days, a need only one team asked for. The source
of the dead work is opposite in each case: in one it is the length of the queue, in the other
it is prioritization itself.
The cost line writes out the product approach’s bill. The work that goes to discovery on the platform side is 108 person-days, that is 2.7 person-quarters. The gap opened in product teams is 128 person-days, 3.2 person-quarters. The platform gains 132 person-days in build work, and the gap written to product teams is 97% of that. Almost everything the platform’s own ledger shows as a gain is paid back in a ledger nobody sees.
The last scan varies the patience window. At a two-quarter patience, the on-demand regime fulfills only seven of the twenty-four requests on time and 254 person-days go to dead work; the same numbers in the product regime are 15 and 40. When the window opens to six quarters, the on-demand regime’s dead work drops to zero, while the product regime’s stays at 24 person-days. For patient teams, waiting in the queue pays off; a need the prioritization pushes to the back does not arrive just because someone waits.
Summary
- The same 24 requests (14 distinct needs, 5 teams, 16 quarters, 26 person-days of capacity per quarter) were run under two regimes: on-demand built 20 capabilities for 416 person-days, product-managed built 14 capabilities for 284 person-days.
- Duplicated build work in the on-demand regime is 132 person-days; four requests are never fulfilled. The product regime fulfills all twenty-four requests.
- Average wait is 3.2 quarters on-demand, 2.3 quarters product-managed, and 0.7 quarters in the discovery-free product run; the 1.6-quarter gap between them is the price of discovery work.
- Adoption per capability rises from 0.80 to 1.43; capabilities never used drop from four (88
person-days) to one (
workload template, 24 person-days). The source of dead work is queue length in one case, priority in the other. billing’s wait rises from 3.2 to 4.4 quarters in the product regime and the requests it adopts fall from four to three: a need only one team asks for falls to the back of the queue.- The product approach costs 108 person-days on the platform side and 128 person-days on the product-team side; the second figure equals 97% of the 132 person-days the platform gains in build work.
Next Step
Both regimes answer the same question: what should the platform do. The product regime tied its answer to the breadth of the need and pushed the minority’s need to the back; the on-demand regime kept the order and paid 132 person-days of duplicated build work. What neither one says is the boundary: what the platform covers and does not cover was never written down anywhere. The next lesson defines that boundary — a path is written as a scope, applied to the five teams’ requirements, the number of teams that fit and what the ones who step outside lose item by item are counted, and then what expanding the path does to maintenance load is scanned.
To keep your progress and take notes, Log in
My notes
Log in to take notes.