Skip to content
academia.sh

Lesson 11 / 12

Migration Strategies

Splitting a platform migration into waves: when five fictional teams migrate in a single wave, the program drops from 65 days to 15 days while the number migrated drops from 4 to 2; the three reasons for being left behind (incompatible requirement, critical period, orphaned service) kick in one after another; dual-run load drops from 105.3 to 0, while peak support load stays under the 6-unit budget only in the single-team wave; and, because of the one team that never migrates, the legacy arrangement stays open indefinitely, costing 41,610 fictional units over 1,095 days.

Published Updated

In this lesson

  • Sweeps the migration program by wave size and counts, for each setting, migrated, left behind, duration, and support load
  • Separates the reason for being left behind by name: incompatible requirement, critical period, orphaned service
  • Measures the long-term cost of an item that never migrates as the two arrangements running together
Contents

The previous lesson split the bill across five teams and counted the item that could not be split. That table’s silent assumption was that all five teams were already on the platform. In reality none of them was born there — every one of them either migrated from somewhere or has not migrated yet. What this lesson measures is that migration itself.

The gradual-migration pattern itself — the strangler fig that wraps around the legacy arrangement and takes its place piece by piece — was measured in M16/K06 and M20/K03; it is not repeated here. The question here is not about the pattern, it is about the schedule: what happens when five teams migrate in groups of a given size.

Splitting the Program into Waves

The example is again the fictional regional measurement network, and the five teams are fictional. The loads, obstacles, and costs are fictional; the currency is fictional too and is called u.

PC31: every team has a migration load, in support-units. PC32: the program is planned as ceil(5 / wave) waves; each wave takes 5 days of prep and 8 days of cutover-and-validate. A team that misses its wave falls to the next one, and if it misses the last wave too it is left behind. PC33: the platform team’s daily support budget is 6 units; if a wave’s load exceeds the eight-day budget, the excess extends the wave. PC34: there are three obstacles — a critical period (cannot migrate between days 0 and 30), an incompatible requirement (can never migrate; the incompatibility surfaces during validation and wastes 40% of the load), an orphaned service (load rises to 1.6 times, and the team migrates only if the wave has slack). PC35: every migrated team produces 0.9 units of daily dual-run load up to the last cutover. PC36: the legacy arrangement’s fixed daily operating cost, and the one-time cost of adding the missing capability to the platform, are both written down.

The plan and the schedule are a model built within the lesson; there is no real migration and no randomness.

// migration/wave.mjs — migration wave model: there is no real migration. Five teams, their
// obstacles, and the platform team's support budget are a data structure. No randomness, the
// plan is entirely deterministic. The program is planned as `ceil(N/w)` waves; a team that
// misses its wave falls to the next one, missing the last wave too leaves it behind.

export const PREP = 5;          // every wave's fixed prep duration (days)
export const BASE = 8;          // every wave's cutover-and-validate duration (days)
export const CAPACITY = 6;      // the platform team's daily support budget (support-units)
export const DUAL = 0.9;        // daily dual-run load per migrated team
export const WASTE = 0.4;       // fraction of load wasted on a failed attempt
export const STEPS = 14;        // infrastructure steps a team would rewrite if it migrated itself
export const REMAINING_STEPS = 4;   // steps left to the team after the platform takes over
export const LEGACY_FIXED = 38;     // the legacy arrangement's fixed daily operating cost (u)
export const CAPABILITY = 2400;     // one-time cost of adding the missing capability to the platform (u)

// team: name, migration load (support-units), obstacle, critical-period window (day range)
export const TEAM = [
  { name: "meter-reading", load: 18, obstacle: null, window: null },
  { name: "validation", load: 14, obstacle: null, window: null },
  { name: "billing", load: 22, obstacle: "critical period", window: [0, 30] },
  { name: "field", load: 26, obstacle: "incompatible requirement", window: null },
  { name: "reporting", load: 16, obstacle: "orphaned service", window: null },
];
export const N = TEAM.length;
export const effectiveLoad = (e) => (e.obstacle === "orphaned service" ? e.load * 1.6 : e.load);
const overlaps = (p, a, b) => p !== null && !(b < p[0] || a > p[1]);

export function plan(w) {
  const count = Math.ceil(N / w), waves = [], migrated = [], leftBehind = [];
  let t = 0, remaining = TEAM.map((_, i) => i), wasted = 0;
  for (let d = 0; d < count && remaining.length; d++) {
    const start = t + PREP, end = start + BASE - 1, selected = [];
    let load = 0;
    for (const i of remaining) {
      if (selected.length >= w) break;
      const e = TEAM[i], effLoad = effectiveLoad(e);
      if (overlaps(e.window, start, end)) continue;    // critical period: falls to the next wave
      if (e.obstacle === "orphaned service" && load + effLoad > CAPACITY * BASE) continue;  // no slack
      selected.push(i); load += effLoad;
    }
    if (!selected.length) { t += PREP + BASE; continue; }
    const delay = Math.max(0, Math.ceil((load - CAPACITY * BASE) / CAPACITY));
    const duration = PREP + BASE + delay, cutover = t + duration - 1;
    for (const i of selected) {
      if (TEAM[i].obstacle === "incompatible requirement") {   // incompatibility surfaces during validation
        leftBehind.push([i, "incompatible requirement"]); wasted += TEAM[i].load * WASTE;
      } else migrated.push([i, cutover]);
    }
    waves.push({ start, end, duration, load, selected });
    remaining = remaining.filter((i) => !selected.includes(i));
    t += duration;
  }
  for (const i of remaining) leftBehind.push([i, TEAM[i].obstacle ?? "no wave left"]);

  const cutovers = migrated.map((x) => x[1]);
  const first = cutovers.length ? Math.min(...cutovers) : 0;
  const last = cutovers.length ? Math.max(...cutovers) : 0;
  const dayLoad = new Array(t + 1).fill(0);
  for (const x of waves) for (let g = x.start; g <= x.end; g++) dayLoad[g] += x.load / BASE;
  for (const [, k] of migrated) for (let g = k + 1; g <= last; g++) dayLoad[g] += DUAL;
  return { w, waves: waves.length, program: t, migrated, leftBehind, wasted, overlap: last - first,
    peak: Math.max(...dayLoad), dual: migrated.reduce((a, [, k]) => a + DUAL * (last - k), 0),
    steps: migrated.length * (STEPS - REMAINING_STEPS) };
}
// migration/measure.mjs — wave-size sweep, why each straggler is left behind, and the cost of never migrating at all.
import { TEAM, N, CAPACITY, LEGACY_FIXED, CAPABILITY, STEPS, REMAINING_STEPS, plan } from "./wave.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 t0 = process.hrtime.bigint();

const W = [1, 2, 3, 5], SETTING = W.map(plan);
console.log(`${N} teams, platform support budget ${CAPACITY} units/day`);

console.log("\n1. wave-size sweep");
const A = [-7, 8, 10, 10, 9, 15, 11, 11];
write(A, "wave", "count", "program", "migrated", "behind", "cutover span", "peak load",
  "dual load");
for (const r of SETTING)
  write(A, r.w, r.waves, `${r.program} days`, `${r.migrated.length}/${N}`, r.leftBehind.length,
    `${r.overlap} days`, r.peak.toFixed(2), r.dual.toFixed(1));
console.log(`  settings exceeding the support budget: ` +
  SETTING.filter((r) => r.peak > CAPACITY).map((r) => `wave=${r.w}`).join(", "));
console.log("  cutover span: from the first cutover to the last — the portion of the time the two");
console.log("  arrangements run together depends on wave size; the legacy arrangement never closes");

console.log("\n2. who is left behind, and why");
const B = [-16, -26, ...W.map(() => 9)];
write(B, "team", "reason", ...W.map((w) => `wave=${w}`));
for (let i = 0; i < N; i++) {
  const row = SETTING.map((r) => r.leftBehind.find((x) => x[0] === i));
  if (!row.some(Boolean)) continue;
  write(B, TEAM[i].name, row.find(Boolean)[1], ...row.map((g) => (g ? "behind" : "migrated")));
}
write(B, "steps recovered", `${STEPS} instead of ${REMAINING_STEPS}`, ...SETTING.map((r) => r.steps));
console.log(`  the incompatibility surfaces during validation: support wasted ` +
  `${SETTING[0].wasted.toFixed(1)} units (same in every setting)`);

console.log("\n3. the long-term cost of never migrating at all (fictional currency)");
const remainingTeams = SETTING[0].leftBehind.length, beforeShare = LEGACY_FIXED / N;
const afterShare = LEGACY_FIXED / remainingTeams, breakeven = Math.ceil(CAPABILITY / LEGACY_FIXED);
const C = [-32, 12, 12, 13];
write(C, "item", "daily u", "365 days", "1095 days");
write(C, "running the legacy arrangement", LEGACY_FIXED, LEGACY_FIXED * 365, LEGACY_FIXED * 1095);
write(C, "adding the missing capability", "-", CAPABILITY, CAPABILITY);
write(C, "difference", "-", LEGACY_FIXED * 365 - CAPABILITY, LEGACY_FIXED * 1095 - CAPABILITY);
console.log(`  breakeven day ${breakeven}; the legacy arrangement stays open for ${remainingTeams} team`);
console.log(`  legacy cost per team ${beforeShare.toFixed(1)} -> ${afterShare.toFixed(1)} ` +
  `u/day (${(afterShare / beforeShare).toFixed(1)}x)`);

const ms = Number(process.hrtime.bigint() - t0) / 1e6;   // raw measurement is not printed, the band is audited
console.log(`\nthree tables and wave sweep in the 2000 ms band: ${ms < 2000 ? "yes" : "no"}`);
5 teams, platform support budget 6 units/day

1. wave-size sweep
wave      count   program  migrated   behind   cutover span  peak load  dual load
1             5   65 days       4/5        1        52 days       5.95      105.3
2             3   39 days       4/5        1        26 days       7.75       46.8
3             2   28 days       3/5        2        13 days       7.25       23.4
5             1   15 days       2/5        3         0 days       7.25        0.0
  settings exceeding the support budget: wave=2, wave=3, wave=5
  cutover span: from the first cutover to the last — the portion of the time the two
  arrangements run together depends on wave size; the legacy arrangement never closes

2. who is left behind, and why
team            reason                       wave=1   wave=2   wave=3   wave=5
billing         critical period            migrated migrated   behind   behind
field           incompatible requirement     behind   behind   behind   behind
reporting       orphaned service           migrated migrated migrated   behind
steps recovered 14 instead of 4                  40       40       30       20
  the incompatibility surfaces during validation: support wasted 10.4 units (same in every setting)

3. the long-term cost of never migrating at all (fictional currency)
item                                 daily u    365 days    1095 days
running the legacy arrangement            38       13870        41610
adding the missing capability              -        2400         2400
difference                                 -       11470        39210
  breakeven day 64; the legacy arrangement stays open for 1 team
  legacy cost per team 7.6 -> 38.0 u/day (5.0x)

three tables and wave sweep in the 2000 ms band: yes

The numbers are of the measurement class; their inputs are the assumptions above.

As the Wave Grows, the Schedule Shortens and the Scope Narrows

The first table lines up four settings side by side, and two columns move in opposite directions. The program’s duration is 65 days at a wave of one team, 15 days at a single wave of five — more than four times shorter. Moving the same direction, the cutover span drops from 52 days to 0, and the dual-run load drops from 105.3 units to 0. A bigger wave shortens the schedule, and there is nothing hidden in that.

The column moving in the opposite direction is the number of teams migrated: it drops from 4/5 to 2/5. The five-team wave shortens the program by more than four times while cutting the scope in half. A migration that finishes in fifteen days is measured by having never done half of the migration at all.

A third column behaves independently of both. Peak support load is 5.95 units at a wave of one, and between 7.25 and 7.75 across the other three settings; the platform team’s daily budget is 6 units. Only wave=1 stays inside the budget. A bigger wave squeezes the same work into a shorter window, and the squeezed work overflows somewhere: in the wave=3 and wave=5 settings, the first wave’s load exceeds the eight-day budget and extends the wave by two days.

There is a part of this table that goes unread: a setting with a cutover span of zero does not mean the two arrangements never ran together. Zero means the two migrated teams hit cutover on the same day. The legacy arrangement does not close in any of the four settings, and the reason is in the second table.

The One Left Behind Has a Name

The second table writes down who is left behind, by name and by reason. Three reasons kick in across three different settings.

Incompatible requirement leaves someone behind in every setting. The field team’s edge nodes keep collecting data even while offline; the platform assumes a constant connection. Wave size never affects this obstacle at all, because the obstacle is not in the schedule — it is in the capability. Because the incompatibility surfaces during cutover validation, 40% of the team’s load, 10.4 units of support, is wasted in every setting.

Critical period kicks in at wave=3 and wave=5. The billing team cannot migrate between days 0 and 30; in a program of two waves or one, no wave falls outside that window. In a five-wave program, the third wave starts on day 31 and the team migrates. Schedule slack is a capacity, and it exists only as the number of waves grows.

Orphaned service kicks in only at wave=5. Two of the reporting team’s services have no owner; the platform team does the work instead, and the load rises to 1.6 times, 25.6 units. In the single-wave program, the other three teams already fill the eight-day budget, so there is no room left for this load.

The last row counts what the platform actually gains for a team: a migrated team hands 10 of its 14 migration steps to the platform, keeping 4 for itself. The gain scales directly with how many teams migrate — 40 steps at wave=1, 20 at wave=5. The field team never gains a single step in any setting.

The abstraction leaks at the cutover span. For that number of days, migrated teams work in the new arrangement while unmigrated ones work in the old, and whoever builds a connection between the two has to know both arrangements’ configuration. The layer the platform claims to have closed reopens for exactly the duration of the migration, and at wave=1 that lasts 52 days. The setting that migrates the most teams is the setting where the abstraction leaks the longest.

The Bill for Never Migrating

The third table asks about a single item: how much never migrating costs. The legacy arrangement’s fixed operating cost is 38 units a day, and it is paid for as long as a single team stays there — 13,870 units over 365 days, 41,610 units over 1,095 days. Adding the missing capability to the platform is a one-time 2,400 units, with a breakeven point at day 64. The three-year difference of 39,210 units shows that this decision is not a budget line item — it is a threshold.

The second number is sharper. While the legacy arrangement was shared by five teams, it came to 7.6 units a day per team; once four teams have migrated, the field team alone carries the same 38 units — five times as much. Migration does not lower the cost for the one left behind, it multiplies it by five. The case where the platform does not pay off shows up here in a single multiplier: the field team has gained 0 steps, spent 10.4 units of support, and multiplied its operating share by five.

Two paths lead out from here, and both are in the table: add the capability to the platform (2,400 units, pays for itself in 64 days), or run the two arrangements indefinitely. A third path — declaring the migration complete — corresponds to none of these numbers.

Summary

  • As wave size climbs from 1 to 5, the program drops from 65 days to 15, the cutover span from 52 days to 0, and the dual-run load from 105.3 units to 0; against that, the number of teams migrated drops from 4/5 to 2/5, and those left behind rise from 1 to 3.
  • Peak support load stays under the daily 6-unit budget only at a wave of one (5.95); the other three settings run between 7.25 and 7.75, and at wave=3 and wave=5 the first wave runs two days long.
  • The three reasons for being left behind kick in at separate settings: incompatible requirement in every setting (field, 10.4 units of wasted support), critical period at wave>=3 (billing), orphaned service only at wave=5 (reporting).
  • The platform’s gain is proportional to how many teams migrate — 10 of 14 steps are handed over, 40 steps at wave=1, 20 at wave=5; a team that never migrates gains not a single step.
  • Because of the one team that never migrates, the legacy arrangement stays open indefinitely: 38 units a day, 41,610 units over 1,095 days. Adding the missing capability is 2,400 units and pays for itself by day 64; the operating share of the one left behind climbs from 7.6 to 38.0 units, a fivefold increase.

Next Step

Six lessons have counted what the platform does: it provisioned environments, generated scaffolds, kept a catalog, embedded rules, split cost, and moved teams in waves. Every lesson also had a counter-number — a leaking case, a team stepping outside, a threshold that did not pay off. The next lesson ties those two columns to a single question: can the gap between teams that adopt the platform and teams that do not be measured, and at what point do the platform’s own metrics start showing that adoption works, not just that adoption happened.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close