Independent engineering project / Migration Wave Planner

Migration Wave Planner

A Python migration planner combining dependencies, capacity constraints, cost models and Monte Carlo scenarios. Completion estimates describe modeled uncertainty, not delivery commitments.

Contribution
Independent design, implementation and evaluation
Languages
Python
Engineering focus
Migration sequencing / Monte Carlo risk / Cost modelling

Scope. Self-directed modernization study measured in a local or simulated environment; not a customer engagement, and not evidence of production migration outcomes.

Implementation & evaluation

The project README is reproduced below, with links to the implementation, design records and operating instructions. Scenario narratives describe an independent project, not a customer deployment.

Read on GitHub

Migration Wave Planner

A planner for a real-shaped problem: a UK general insurer wants to move 29 on-premises components to Azure, and somebody has to say which things move together, in what order, and what the arrangement costs.

The interesting part is not the schedule. It is that the first honest answer to "give me the migration plan" is the plan you asked for does not exist, and the second is "best found" is a claim that needs a number attached to it. Everything in docs/results.md is generated by running the model; no figure in this repository was typed by hand.

python run_planner.py            # regenerate docs/results.md
python -m pytest tests -q        # 436 tests
./test.ps1                       # tests + byte-identical rebuild of the report
./demo.ps1 -Section 6            # print one section

The problem

An insurer's estate is not a set of services with clean interfaces. It is a policy admin system whose batch layer writes directly into the same schema the online system reads, a claims database three applications share, a message hub everything talks to, and a warehouse that feeds a report that feeds an operational screen. Twenty-nine components, fifty-four dependencies, six delivery teams, 1,610 person-days of work.

Migration in waves is the standard answer. A wave is a set of components that cut over together. Between waves, anything with one end migrated and the other not needs a temporary hybrid link — a VPN route, a firewall change, a shim, and the on-call attention that hybrid links always attract. Every wave boundary costs money and creates a window where something can break at three in the morning.

So the planner has to answer four questions at once:

question where it is answered
Which components cannot be separated at all? wave/units.py
What does an arrangement cost? wave/costs.py
Which arrangement is cheapest? wave/solvers.py
How good is "cheapest" — against what? wave/bounds.py

And then two more that the business case usually leaves out:

What is the date, given that estimates are estimates? wave/risk.py
Who is exposed while the estate is half-migrated? wave/blast.py

What the model actually says

Read docs/results.md for the full run. The findings that changed my mind while building it:

Three components cannot be migrated at all, as declared. Contracting the estate over its unsplittable dependencies — shared schemas, replication feeds — produces units of 225, 245 and 190 person-days against wave capacities of 100, 130 and 120. There is no valid plan. The planner's first output is a £183,000 remediation programme that has to happen before any wave plan is meaningful, and the honest deliverable is that number, not a Gantt chart.

The cheapest way to buy feasibility is not the cheapest set of edges. A greedy cheapest-first decoupling pays £67,000 for the claims cluster where £38,000 buys the same feasibility, because breaking the cheapest edge leaves the remaining unit still over capacity — you pay for a cut that bought nothing.

Most of the money is not where the migration conversation puts it. Egress charges — the thing every cloud discussion starts with — are £212 across the whole programme, 0.007% of it. The hybrid links that carry those bytes cost £241,329 to build and run: 1,136 times the value of the traffic flowing over them. The cost of being half-migrated is the cost of the connection, not the data.

The licence renewal calendar is a planning variable, and ignoring it costs 17.8% of discretionary spend. A planner blind to renewal dates produces a plan that is £62,669 better on every other term and still loses by £173,365, because it strands four components one month past a renewal they had already been paid for.

Merge bias comes from balance, not headcount. The number of near-critical work streams in a wave predicts its schedule bias with a rank correlation of 0.975; the number of teams predicts it at 0.821. Two waves in the best plan run five teams each and differ by 2.4x in bias.

Change freezes are a cost line, not a hedge. They add 2.10 months (21.3%) and the standard deviation of the end date rises, from 2.435 to 2.675. They also break the P50: freezes make the outcome distribution mixed rather than continuous, and on one of the four plans 30.4% of all outcomes land on exactly the same month — the median falls inside that atom and reports a slip of +0.00 months while the mean slip is +0.84.

The cheapest plan is the most exposed. The annealed plan has the highest peak blast radius of the four (92 against 63–73) while being £250,000 cheaper than the next. Cost and exposure rank the plans differently; three of the four sit on the frontier. A programme tracking only one of these two numbers is choosing an answer rather than measuring one.


The discipline

Eleven of twelve written predictions in docs/results.md were wrong. That is not modesty — it is the mechanism.

wave/report.py implements a small DSL where found() raises if no expect() preceded it, and render() raises if a prediction was left open. Every claim in the report is a formatted computed value, never a literal. That constraint caught five factual errors in prose that had gone stale while the numbers underneath it moved, including a paragraph explaining an atom of probability at the median on a run where the median was not in an atom.

docs/results.md is a build artefact and is checked like one: tests/test_results_integrity.py regenerates it in a temporary directory and compares the sha256 against a value committed alongside the model. That check found a wall-clock timing column that made the report non-reproducible.


Bugs the tests found

Writing tests after the model, against a report that already existed, found five defects the report had been quietly reporting around. They are written up in docs/portfolio/04-bugs-the-experiment-found.md; two are worth naming here because they are the same mistake:

  • _hybrid_build_bound and cheapest_feasible_decoupling both enumerated subsets by cardinality and stopped at the first feasible size while documenting an optimality contract. Feasibility is monotone in cardinality; cost is not. Both now enumerate the full lattice.
  • _dependents memoised a transitive closure whose value depended on the cycle stack it was computed under, so the blast radius of a component depended on the order components appear in a source file.

Layout

wave/estate.py      the declared estate + 13 construction invariants
wave/units.py       union-find contraction; exact cheapest decoupling
wave/costs.py       what an arrangement costs, and when it lands
wave/solvers.py     three constructive heuristics + simulated annealing
wave/bounds.py      a lower bound, and exact optima on sub-instances
wave/risk.py        two-level correlated Monte Carlo over team effort
wave/blast.py       exposure, live links, and the cost/exposure frontier
wave/report.py      the expect/found DSL
run_planner.py      the single source of every number in docs/results.md

Pure standard library plus numpy. No network, no services, no database.

  • docs/adr/ — five decisions and what each one cost
  • docs/known-limitations.md — what this model does not do
  • docs/portfolio/ — the argument, in prose

Source & documentation

Continue into the implementation.

The source repository contains setup instructions, design decisions, evaluation guidance and the project's stated limitations.

Browse supporting documentation 11 documents / 5 decision records