Every legacy modernization plan fails the same way.
Here's the plan that doesn't.
Most legacy rewrites don't fail on the new technology. They fail on step one, the one everyone skips because it doesn't look like progress. The five-phase plan we run before touching a single line of the old system.
I've been called in after enough failed rewrites to recognize the shape of the mistake before anyone finishes describing it. It's almost never the new stack. So what is it? The plan started with the new system instead of the old one, and by the time that gap surfaces, it's already expensive to fix.
01 · Why most legacy modernization plans fail before they start
The instinct is to scope the replacement off the original spec, the architecture diagram, the documentation, whatever official description of the system exists. That description was accurate once. It stopped being accurate the first time someone patched a bug at 2am and never wrote it down, and every system old enough to be called "legacy" has years of those patches baked into what it actually does. The real specification isn't the document. It's the system's current behavior, undocumented parts included.
“Every legacy system has two specs: the one someone wrote down, and the one it's actually running. Rewrite against the first and you'll ship something that's correct and wrong at the same time.”
02 · The plan, in five phases
- 01Reverse-engineer real behavior, not documentation. Interview the operators who work around the system daily, trace what it actually does in production, and catalog every undocumented workaround before a line of the replacement gets written. This phase produces no visible progress and is the one most plans skip to save time. It's also the one that determines whether the other four phases work.
- 02Turn that behavior into a contract, not a wishlist. Build a characterization suite, real inputs, real outputs, edge cases included, that captures exactly what the current system does today. The replacement's job is to satisfy this contract, not to satisfy someone's memory of what the system was supposed to do.
- 03Build the replacement in parallel, not in place. The new system runs alongside the old one, shadow-processing the same real traffic, without yet making a single decision that matters. This is the phase where the gap between "looks done" and "is done" gets found while it's still cheap to fix.
- 04Validate behavior-for-behavior against real traffic. Compare old versus new output against production data over a meaningful window, not a QA environment stocked with synthetic test cases nobody's actual customer will ever send. Synthetic data can't find the input nobody thought to imagine.
- 05Cut over in reversible slices. Shift traffic or workload gradually, one slice at a time, each with a rollback path. A mismatch in one slice is a five-minute revert. A mismatch after a single weekend cutover is an incident review with your name on it.
$ shadow-run --old=legacy --new=replacement --window=30drequests compared ................. 2,140,880output mismatches (week 1) ........ 1,204root cause: rounding rule undocumented in spec, present in legacy since 2019output mismatches (week 4) ......... 3root cause: known edge case, fix shipped, re-verifyingcutover readiness .................. PASS (0 unexplained mismatches, 14 days)# week 1 is why phase 1 exists. this system had a rule nobody remembered writing.
// the 1,204 mismatches in week one weren't bugs in the new system. they were undocumented behavior in the old one, finally made visible instead of rediscovered in production six months after go-live.
03 · The part every plan underestimates: the data itself
Ask any team mid-rewrite where the timeline actually went, and the honest answer is rarely the new application code. It's the data underneath it: fifteen years of records spread across formats that were never meant to coexist, a schema that's been through six undocumented revisions, timestamps in three different timezones depending on which era of the system wrote them, and no one left on staff who can say with confidence which of it still matters.
This is the pain point clients describe first, once they trust us enough to stop describing the project in roadmap language. Not "we need a modern stack." "We have twenty years of data, in different formats, on different timelines, and we genuinely don't know how much of it is duplicate, dead, or load-bearing." A CRM export from 2009 sitting next to a schema-v7 production table sitting next to a spreadsheet someone built as a "temporary" fix in 2014 and never retired, all describing the same customer, disagreeing with each other about basic facts.
“Not all data deserves to survive the migration. Migrating it anyway just moves the mess into a system that was supposed to be clean.”
Migrate everything as-is and the new system inherits every duplicate, every dead table, every format inconsistency the old one accumulated, clean architecture built on top of the same unresolved mess. Try to sort it manually and the project stalls, because no team can hand-review millions of records across a dozen inconsistent sources on any timeline a business will accept. This is exactly where AI-assisted data profiling and a real data quality plan earn their place in phase one, not as a nice-to-have, as the thing that makes the rest of the plan possible.
$ profile --sources=7 --window=full-historysource: legacy_crm.csv ............ fixed-width, 1997–2009source: orders_db (schema v3) ..... normalized, 2009–2016source: orders_db (schema v7) ..... normalized, 2016–presentsource: billing_export.xlsx ....... ad hoc, "temporary" since 2014duplicate customer records (fuzzy match) .... 14,220 (~6.1% of total)tables with zero reads in 18 months .......... 41 of 96fields with inconsistent units or format ..... 23# nobody decided to create this mess. it accumulated one exception at a time.$ triage --strategy=ai-assistedmust-migrate (active, business-critical) .... 31 tablesarchive (historical, rarely accessed) ........ 19 tablesmerge candidates (fuzzy-matched duplicates) .. 14,220 recordsdrop (confirmed unused, no dependency) ....... 46 tables# the plan isn't "migrate everything." it's "migrate what still matters, on purpose."
None of that triage runs on exact-match rules alone, because the real world doesn't spell things consistently. "Bob Smith" and "Robert Smith, Jr." and a record with a transposed digit in the same phone number are the same customer to a person and three different rows to a naive deduplication script. We use similarity- and pattern-matching models to catch what exact matching misses, usage and query-log analysis to tell active data from the tables nobody has touched in years, and the same data quality scoring that ran the initial triage stays on after cutover, so the new system doesn't quietly re-accumulate the mess it was built to escape. It's the same discipline behind our data engineering and quality work generally, just aimed at the specific pile of data one particular legacy system left behind.
// most legacy rewrites don't inherit a system. they inherit twenty years of undisclosed decisions about what counted as data, and nobody budgeted time to sort that out until it was blocking the cutover.
04 · The step everyone wants to skip
Phase one has no demo, no visible milestone, and no line on a roadmap that looks like momentum, which is exactly why it's the phase that gets compressed or cut under deadline pressure. Every rewrite with a reputation for quietly stalling, or shipping and then getting rolled back, skipped or rushed the same step: understanding what the system actually does before deciding what it should do instead.
So the plan that works isn't the exciting one. It's the one that spends real time in the unglamorous first phase, validates against real traffic instead of a demo, and cuts over in pieces small enough that being wrong about one of them costs an afternoon instead of a quarter. That's the whole plan. The hard part was never the technology. It was being willing to start with the boring phase first.
$ end of file · written by flux/dk · 2026-05-25