Migration Runbooks
Jul 25, 2023138 min read

Migration Runbooks: Deep Dive #68

AV
Ariel Voss

Principal React Architect

This post follows the plan’s “Migration Runbooks” focus and global requirements:

This is a real migration playbook style. If other pages use conceptual APIs, treat them as illustrative; the operational steps here are standard.

A migration runbook is a sequence you can execute. It’s not a collection of principles. The goal is to reduce risk by making decisions explicit, measurable, and reversible.

The runbook structure that works best:

  1. Define what “done” means (exit criteria).
  2. Define invariants that must not change (URLs, metrics, accessibility).
  3. Migrate in slices with feature flags.
  4. Capture evidence (logs, snapshots, metrics).
  5. Roll back quickly if invariants break.

Worked Example: Migrating a Route Tree Without Breaking URLs

We’ll assume you have an existing route /settings with sub-pages, and you’re migrating to a new layout + component structure while keeping URLs stable.

Step 1: Inventory and Invariants

Write down what you cannot break.

export type MigrationInvariant = {
  id: string;
  description: string;
  verify: () => Promise<void> | void;
};

export const invariants: MigrationInvariant[] = [
  { id: "url:/settings", description: "URL remains reachable", verify: () => {} },
  { id: "a11y:focus", description: "Focus order remains usable", verify: () => {} },
  { id: "perf:p95", description: "P95 navigation time does not regress", verify: () => {} },
];

Step 2: Feature Flag the New Surface

export type Flags = {
  newSettingsShell: boolean;
};

export function getFlags(): Flags {
  return { newSettingsShell: process.env.NEXT_PUBLIC_NEW_SETTINGS_SHELL === "1" };
}

Step 3: Keep the URL, Swap the Implementation

import { getFlags } from "./flags";
import LegacySettings from "./legacy-settings";
import NewSettingsShell from "./new-settings-shell";

export default function SettingsRoute() {
  const flags = getFlags();
  return flags.newSettingsShell ? <NewSettingsShell /> : <LegacySettings />;
}

Step 4: Runbook Evidence (Transcript + Snapshot)

Capture a small amount of stable evidence per slice:

export type RouteTranscript = {
  route: string;
  variant: "legacy" | "new";
  timestamp: number;
  notes: string[];
};

export function makeTranscript(route: string, variant: "legacy" | "new", notes: string[] = []): RouteTranscript {
  return { route, variant, timestamp: Date.now(), notes };
}
export function TranscriptPanel({ transcript }: { transcript: RouteTranscript }) {
  return (
    <aside data-component="TranscriptPanel">
      <h2>Migration Transcript</h2>
      <pre>{JSON.stringify(transcript, null, 2)}</pre>
    </aside>
  );
}

Step 5: Rollback Plan (Always)

Your rollback plan should be a single action:

  • flip the flag off
  • (optionally) clear any persisted state that only the new surface writes

If rollback requires code changes, the runbook is incomplete.

Runbook Checklist

  • Keep URLs stable (public contract).
  • Use feature flags to switch surfaces.
  • Migrate in slices; gather evidence each slice.
  • Define rollback as a single action.