Skip to content
academia.sh

Lesson 10 / 10

Operations Handover

The responsibility contract between development and operations: twenty of twenty-eight responsibility items are handed over and eight stay with development for lack of information, tooling, or a runbook; the same fourteen events close in 3.8 rounds under shared on-call versus 5.0 under full handover, while the interruption cost pulled from the development flow rises from 37 to 54 and rework from 9 to 20; once three undocumented runbooks are written, escalations fall from 8 to 5 and the flow end from round 94 to 84.

Contents

The previous lesson tied scope change to control and counted its cost, but it stopped the counter the moment the item was delivered. Delivery is the last step finishing in the model; in the field it is the first day the service also has to stay up at night.

Operations handover is a service’s operating responsibility moving from the team that wrote it to the team that operates it, and it is not one piece — it is handed over item by item. This lesson models the handover as a contract: which item was handed over, why an unhandled one could not be, and where its bill lands.

The Handover Contract, Item by Item

The example is the course’s fictional regional library network and the three teams that build it. TD28: seven services are split across three writing teams, plus one operations team; three teams together touch the shared-registry service. Each team has six items ahead of it, six rounds of work per item. TD29: the day is ten rounds; the first six rounds are the workday, the last four are night, and an item only advances during the workday. TD30: the contract is four responsibility items — operation, first response, restart, configuration change; the reason an item cannot be handed over is a lack of information, tooling, or a runbook. TD31: a critical event is met at night too, a minor event waits until morning. TD32: the interrupted team is pulled out of the flow for the response time plus two rounds of recovery; if the interruption lands in the middle of an item, at most two rounds of rework are written, and a team woken at night loses two rounds the next morning. TD33: the operations team closes a handed-over responsibility in two rounds; because the context has to be rebuilt secondhand, resolving an escalated event costs the writing team three rounds (two under shared on-call).

The contract and the two on-call policies are an in-process model; there is no real on-call schedule or incident log.

// handover/oncall.mjs — the fictional library network's operations handover and on-call model
// (model): no real on-call schedule or incident log exists, rounds are a data structure. No randomness.

export const DAY = 10, WORKDAY = 6;         // each day is 10 rounds: first 6 rounds workday, last 4 night
export const FIRST = 1, WAKE = 2, TRANSFER = 1, RESOLVE_OPS = 2, RESOLVE_BUILD = 3, RESOLVE_LOCAL = 2;
export const RECOVERY = 2, SLEEP_COST = 2, REWORK_CAP = 2;
export const ITEM_WORK = [6, 6, 6, 6, 6, 6];   // six items ahead of each writing team
export const TEAMS = ["E1", "E2", "E3"];

export const isNight = (t) => t % DAY >= WORKDAY;
export const nextMorning = (t) => (Math.floor(t / DAY) + 1) * DAY;

const table = (s, f) => s.trim().split("\n").map((r) => f(r.trim().split(/\s+/)));

// Handover contract: service, owning team, number of teams touching it, four responsibilities. "+" is
// handed over; anything else is not handed over, and names the reason (information, tooling, runbook).
export const CONTRACT = table(`
  loan             E2  1  +  +  +            information
  fee              E2  1  +  +  +            +
  branch-front     E1  1  +  +  +            +
  membership-gate  E1  1  +  +  tooling      tooling
  catalog-gate     E3  1  +  +  +            runbook
  notification     E3  1  +  +  runbook      runbook
  shared-registry  E3  3  +  +  information  information`,
([service, team, teams, ...h]) => ({ service, team, teams: +teams,
  status: { operation: h[0], "first-response": h[1], restart: h[2], configuration: h[3] } }));

// Event: arrival round, service, responsibility needed to close it, severity.
export const EVENT = table(`
   3  loan             configuration  critical
   7  fee              restart        critical
  12  shared-registry  configuration  critical
  16  notification     restart        critical
  18  branch-front     configuration  critical
  23  membership-gate  restart        critical
  27  catalog-gate     configuration  minor
  31  loan             restart        critical
  36  shared-registry  restart        critical
  39  notification     configuration  critical
  44  fee              configuration  critical
  47  membership-gate  configuration  minor
  50  catalog-gate     restart        critical
  52  branch-front     restart        minor`,
([round, service, resolution, a]) => ({ round: +round, service, resolution, critical: a === "critical" }));

// policy: "full" means the operations team meets every event, "shared" means the writing team is on call.
// closeReason: responsibility items not handed over for this reason are treated as closed (counter-run).
export function run(policy, closeReason = null) {
  const S = Object.fromEntries(CONTRACT.map((s) => [s.service, s]));
  const team = Object.fromEntries(TEAMS.map((e) =>
    [e, { busy: 0, i: 0, progress: 0, diverted: 0, rework: 0, finish: null }]));
  const pending = {}, log = [];
  const schedule = (e, t, duration) => ((pending[t] ??= []).push([e, duration]));
  const isOpen = (s, c) => s.status[c] === "+" || s.status[c] === closeReason;

  const handle = (o, t) => {
    const s = S[o.service];
    if (policy === "full" && isOpen(s, o.resolution))       // resolved by ops via a written runbook
      return { s, first: FIRST, closure: FIRST + RESOLVE_OPS, handoffs: 1, escalated: false,
        woke: false, interruption: 0, reason: "-" };
    const escalated = policy === "full";
    const handoffs = (escalated ? 2 : 1) + (s.teams > 1 ? 1 : 0);
    const transfer = TRANSFER * (handoffs - 1), resolve = escalated ? RESOLVE_BUILD : RESOLVE_LOCAL;
    const firstResponse = escalated ? FIRST : isNight(t) ? (o.critical ? WAKE : nextMorning(t) - t) : FIRST;
    const reason = escalated ? s.status[o.resolution] : "-";
    const common = { s, first: firstResponse, handoffs, escalated, reason };
    if (!isNight(t)) {
      const interruption = (escalated ? 0 : FIRST) + transfer + resolve + RECOVERY;
      schedule(s.team, t, interruption);
      return { ...common, closure: firstResponse + transfer + resolve, woke: false, interruption };
    }
    if (o.critical) {                                        // night wake-up: the cost lands next morning
      schedule(s.team, nextMorning(t), SLEEP_COST);
      return { ...common, closure: (escalated ? FIRST + WAKE : WAKE) + transfer + resolve, woke: true,
        interruption: SLEEP_COST };
    }
    const interruption = transfer + resolve + RECOVERY;      // a minor night event waits until morning
    schedule(s.team, nextMorning(t), interruption);
    return { ...common, closure: (escalated ? nextMorning(t) - t : firstResponse) + transfer + resolve, woke: false, interruption };
  };

  const events = policy === "none" ? [] : EVENT;
  for (let t = 0; t < 200; t++) {
    for (const o of events.filter((x) => x.round === t)) log.push({ o, ...handle(o, t) });
    for (const [e, duration] of pending[t] ?? []) {
      const k = team[e];
      k.busy += duration;
      const d = Math.min(k.progress, REWORK_CAP);            // interruption landed mid-item
      k.progress -= d; k.rework += d;
    }
    if (isNight(t)) continue;                                // items do not advance outside the workday
    for (const e of TEAMS) {
      const k = team[e];
      if (k.finish !== null) continue;
      if (k.busy > 0) { k.busy--; k.diverted++; continue; }
      if (++k.progress < ITEM_WORK[k.i]) continue;
      k.progress = 0; k.i++;
      if (k.i >= ITEM_WORK.length) k.finish = t + 1;
    }
  }
  const total = (f) => TEAMS.reduce((a, e) => a + f(team[e]), 0);
  const avg = (f) => (log.length ? log.reduce((a, x) => a + f(x), 0) / log.length : 0);
  return { log, team,
    first: avg((x) => x.first).toFixed(1), closure: avg((x) => x.closure).toFixed(1),
    handoffs: log.reduce((a, x) => a + x.handoffs, 0), escalated: log.filter((x) => x.escalated).length,
    woke: log.filter((x) => x.woke).length,
    diverted: total((k) => k.diverted), rework: total((k) => k.rework),
    end: Math.max(...TEAMS.map((e) => team[e].finish)) };
}
// handover/measure.mjs — handover contract, two handover policies, cost of what cannot be handed over, team boundary.
import { run, CONTRACT, EVENT, isNight } from "./oncall.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const avg = (a) => (a.length ? (a.reduce((s, x) => s + x, 0) / a.length).toFixed(1) : "-");
const RESPONSIBILITY = ["operation", "first-response", "restart", "configuration"];

const items = CONTRACT.flatMap((s) => RESPONSIBILITY.map((c) => s.status[c]));
const reasons = ["information", "tooling", "runbook"];
const count = (x) => items.filter((k) => k === x).length;
console.log(`contract: ${items.length} items, handed over ${count("+")}, not handed over ` +
  `${items.length - count("+")} (${reasons.map((n) => `${n} ${count(n)}`).join(", ")})`);
const R = { none: run("none"), full: run("full"), shared: run("shared"),
  withRunbooks: run("full", "runbook") };
console.log(`\n${EVENT.length} events, ${EVENT.filter((o) => isNight(o.round)).length} outside the workday, ` +
  `${EVENT.filter((o) => o.critical).length} critical; without events the flow ends at round ${R.none.end}`);

const O = [-34, 15, 17, 29];
console.log("\n1. the same set of events under two handover policies (third column: counter-run)");
write(O, "measure", "full handover", "shared on-call", "full handover + 3 runbooks");
const MEASURES = [["time to first response (rounds)", "first"], ["closure delay (rounds)", "closure"],
  ["handoffs", "handoffs"], ["escalations", "escalated"], ["out-of-hours pages", "woke"],
  ["interruption cost (rounds)", "diverted"], ["rework (rounds)", "rework"],
  ["end of development flow", "end"]];
for (const [name, a] of MEASURES) write(O, name, R.full[a], R.shared[a], R.withRunbooks[a]);

const N = [-13, 8, 14, 22, 21];
console.log("\n2. why events escalate under full handover");
write(N, "reason", "events", "avg. closure", "interruption cost", "out-of-hours pages");
for (const n of reasons) {
  const g = R.full.log.filter((x) => x.reason === n);
  write(N, n, g.length, avg(g.map((x) => x.closure)), g.reduce((s, x) => s + x.interruption, 0),
    g.filter((x) => x.woke).length);
}

const M = [-13, 8, 14, 21];
console.log("\n3. does the service overlap the team boundary? (full handover)");
write(M, "service", "events", "avg. closure", "handoffs per event");
for (const [name, f] of [["single-team", (x) => x.s.teams === 1],
  ["multi-team", (x) => x.s.teams > 1]]) {
  const g = R.full.log.filter(f);
  write(M, name, g.length, avg(g.map((x) => x.closure)), avg(g.map((x) => x.handoffs)));
}
contract: 28 items, handed over 20, not handed over 8 (information 3, tooling 2, runbook 3)

14 events, 7 outside the workday, 11 critical; without events the flow ends at round 56

1. the same set of events under two handover policies (third column: counter-run)
measure                             full handover   shared on-call   full handover + 3 runbooks
time to first response (rounds)               1.0              1.6                          1.0
closure delay (rounds)                        5.0              3.8                          4.1
handoffs                                       24               16                           21
escalations                                     8                0                            5
out-of-hours pages                              3                5                            1
interruption cost (rounds)                     37               54                           27
rework (rounds)                                 9               20                           10
end of development flow                        94              105                           84

2. why events escalate under full handover
reason         events  avg. closure     interruption cost   out-of-hours pages
information         3           6.3                    15                    1
tooling             2           6.0                    12                    0
runbook             3           7.0                    10                    2

3. does the service overlap the team boundary? (full handover)
service        events  avg. closure   handoffs per event
single-team        12           4.7                  1.5
multi-team          2           7.0                  3.0

The numbers are in the measurement class; their inputs are the assumptions above. Twenty of the twenty-eight items are handed over, and the contract table says this: operation and first response are handed over in all seven of the seven services, and the entire unhandled eight sit in the restart and configuration columns. The handover does not stop at keeping the service running — it stops where the service needs to be touched.

Full Handover versus Shared On-Call

The first table’s first two columns meet the same fourteen events under two policies. Under full handover, the operations team is on call, and time to first response is 1.0 rounds with no day-night distinction; under shared on-call, the writing team gives the first response and the delay rises to 1.6 rounds, because a minor event arriving at night waits until morning. At closure the order reverses: shared on-call closes in 3.8 rounds, full handover in 5.0. The difference is that eight events cannot be resolved and escalate back to the writing team; every escalation is a handoff and a context rebuilt secondhand (24 handoffs against 16).

The bill is in the rows below. With no events, the three teams finish their work at round 56; under full handover the flow stretches to round 94, under shared on-call to round 105, interruption cost is 37 against 54, and rework is 9 against 20. Out-of-hours pages are also higher under shared on-call (5 against 3): under full handover, only the critical night event that cannot be handed over wakes anyone. Shared on-call closes events faster and pays for it out of the flow.

The Cost of What Cannot Be Handed Over

The second table splits the eight escalated events by reason. The most expensive reason is the undocumented runbook: three events, an average closure of 7.0 rounds, and two of the three out-of-hours pages. Missing information accounts for three events at 6.3 rounds and produces the highest interruption cost (15); missing tooling accounts for two events at 6.0 rounds.

The first table’s third column gives the price of the runbook: once three runbooks are written, escalations fall from 8 to 5, out-of-hours pages from 3 to 1, interruption cost from 37 to 27, and the flow end from round 94 to 84. An undocumented runbook is not a missing document — it is ten rounds of flow delay and two out-of-hours pages.

Which Team Boundary an Event Falls Into

The third table splits events by whether the service overlaps the team boundary. In single-team services, twelve events close in 4.7 rounds and 1.5 handoffs per event; in the shared-registry service that three teams touch, two events close in 7.0 rounds and 3.0 handoffs. The reason is in the contract: both of that service’s unhandled items are for the reason information, because information does not stay inside a single team. A service that does not overlap the team boundary is expensive not only while being built, but at night too.

Summary

  • The handover contract splits into 28 items; 20 are handed over and 8 are not (information 3, tooling 2, runbook 3), and all of the unhandled items sit in the restart and configuration columns.
  • The same 14 events close in 5.0 rounds under full handover and 3.8 under shared on-call; time to first response is reversed (1.0 against 1.6), and handoffs are 24 against 16.
  • The cost is in the development flow: with no events the flow ends at round 56, but it stretches to 94 under full handover and 105 under shared on-call; interruption cost is 37 against 54 and rework is 9 against 20.
  • Once three undocumented runbooks are written, escalations fall from 8 to 5, out-of-hours pages from 3 to 1, and the flow end from round 94 to 84; the service that three teams touch closes its events in 7.0 rounds and 3.0 handoffs (against 4.7 and 1.5 in a single-team service).

Course Wrap-Up

Lesson Flow Object Handoffs / Waiting / Rework Cost of Architectural Alignment
Lifecycle Models 18 items, 4 cycles 125 → 120 / 243 → 18 rounds / 105 → 90 units wrong-boundary delay 14.8 → 7.8 rounds
Agile Frameworks 30 items, 1–6 weeks carry-over 8 → 0 / flow 12.5 → 31.2 days / 19 → 23 person-days 75 linked pairs determine rework
Flow-Based Management 60 items, limit 1–16 lead time 118.5 → 17.3 days / waiting share 0% → 84.1% the limit is the WIP limit, not the team; 195.9 person-days go to switching
Scaled Frameworks 40 items, 1–12 teams coordination rounds 0 → 66 / per item 0 → 5.9 periods shared channels grow quadratically; lowest cost at 3 teams
Engineering Practices 48 items, 2–40 hours incidents 21 → 2 / delay 8.0 → 21.4 hours / 82 → 132 hours rework grows with same-module items
Conway’s Law 12 items, two splits 24 → 17 / 203 → 55 rounds / 24 → 15 boundary-crossing links 40/43 → 32/43; pathless cross-team links 16 → 0
Team Topologies 12 items, three arrangements 24 / 17 / 25 — 203 / 55 / 78 rounds — 24 / 15 / 22 cognitive load falls from 1–13 to 3–9, duplicated expertise 18 / 18 / 24
Estimation and Planning duration of 12 items folding waiting into the estimate takes error from 58.4% to 23.5% at 3+ teams, waiting share is 51.7% and error 72.2% (24.3% and 44.6% at 1–2 teams)
Change Management 8 items, 6 requests 31 / 38 — 44 / 133 rounds — 8 / 4 rounds a request that falls outside the boundary makes delivery 28 rounds instead of 23
Operations Handover 14 events, 28 items 24 / 16 — closure 5.0 / 3.8, interruption cost 37 / 54 — 9 / 20 rounds the service that three teams touch has 3.0 handoffs instead of 1.5

This is the rule of the Process, Team and Delivery course: process and team decisions shape the architecture from wherever the work is waiting. No lesson found a process good on its own; each one was run on the same set of work items, and what it cost where it won was counted.

The Software Architecture curriculum ends here. The five courses’ five measurement axes come together in one sentence: a decision is an object and its traceability is measured (The Architect’s Role), a document is measured by the question it answers (Architectural Decisions and Documentation), at the scale of the enterprise the measure is a link (Enterprise Context and Integration), a rule is governable to the extent it can be checked by machine (Quality Attributes and Governance), and process and team decisions are measured from wherever the work is waiting (Process, Team and Delivery).

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close