Ground Truth

A Migration Is Not a Build: The Old System Knows Things Nobody Wrote Down

Mostafa DhouibMostafa Dhouib·7 min read
The short answer

Replacing a long-lived platform gets scoped as a greenfield build because the new stack is the exciting part. What actually has to move is fifteen years of business rules encoded in stored procedures and triggers, which no specification describes because nobody ever wrote them. The first deliverable is proving that knowledge moves, on a copy, before anything depends on it having moved.

A Migration Is Not a Build: The Old System Knows Things Nobody Wrote Down

The short answer. When a long-lived business platform is replaced, the brief is almost always written as a build: a new stack, a new interface, a feature list. The actual work is a migration, and the thing being migrated is not the data. It is the behaviour encoded in stored procedures, triggers, constraints and defaults, accumulated over years by people who have left, which no specification describes because nobody ever wrote it down. A replacement built from the visible behaviour reproduces what people remember rather than what the system does, and the gap between those two is discovered in production. The first deliverable is an inventory of what the database actually enforces, proven to reproduce on a copy, before anything is built that assumes it did.

There is a specific project that goes wrong in a specific way, often enough to be predictable.

A company has run the same platform for a decade or more. It is old, the framework is long out of support, and the people who maintain it are expensive and scarce. So the company scopes a replacement: modern stack, modern interface, the same functionality.

The same functionality is the problem, because nobody can say what it is.

Where the rules actually live

In systems of this age, a great deal of business logic sits in the database rather than in the application.

Stored procedures carry multi-step operations with branching that encodes real policy. Triggers fire on write and quietly maintain invariants, write audit rows, cascade updates or reject operations that violate rules nobody has articulated in years. Constraints and defaults encode assumptions. Views carry definitions of what a customer, an active account or a billable item actually is, and those definitions have been tuned repeatedly in response to real problems.

None of that is visible from the application. A developer reading the application code sees a call to a procedure and a result. The policy inside is invisible, and it is frequently the most business-critical code in the organisation.

So the specification for the new system, written by interviewing users and reading the interface, describes the behaviour people notice. The system's actual behaviour includes everything it does that nobody notices because it has always been right.

What users noticethis is what the new specification describes
Application codethe policy inside the call is invisible from here
Stored proceduresreal business rules, undocumented
Triggersthey also reject operations against rules nobody has articulated in years
Constraints and defaults
Viewsdefinitions tuned repeatedly in response to real problems
The first deliverable is an inventory of this, taken against a copy with nothing live touched.
FigureThe specification written by interviewing users and reading the interface describes the top layer, because that is the only layer anybody can see. Everything below it was written by people who have left, and it is frequently the most business critical code in the organisation. A replacement built from the visible behaviour reproduces what people remember rather than what the system enforces.

Why a rewrite discovers this at the worst time

A full rewrite with a single cutover concentrates all of that risk at one moment.

Every missing rule is found in production, by a user, after the old system is gone. The organisation cannot fall back, because falling back means reverting however long of new data. And each discovery arrives as an incident rather than as a finding.

There is an additional problem with the economics. A rewrite delivers nothing until it delivers everything, so if it runs long, and these run long, there is no partial value and no natural point to stop. This is exactly why the major platform vendors recommend incremental replacement over full rewrites for production applications: the risk is not technical difficulty, it is that a rewrite cannot be stopped halfway.

The first gate: inventory on a copy

The useful first deliverable produces no new features at all.

Take a copy of the production database. Touch nothing live. Then inventory what it does on its own: enumerate the procedures, triggers, constraints, defaults and views, and for each one establish what it enforces and whether anything in the new design accounts for it.

Running on a copy is what makes this work. There is no risk, so the work can be thorough, and thoroughness is the entire value. It also produces an artefact the organisation has never had: a written description of its own rules.

The output is a catalogue, and more importantly a list of rules with no owner in the new design. That list is the real finding, and it is usually longer than anyone expects.

The traps that load cleanly

Alongside the rules, there is a category of migration failure that produces no error at all, and locale is where most of it lives.

Calendar conventions. Where a system stores dates in a non-Gregorian calendar, reading them as Gregorian shifts every date by a fixed offset, which can be centuries. Nothing rejects a valid date. Every record loads. The data is wrong in a way that looks like data.

Character encoding. Text moving between a legacy code page and a modern encoding corrupts silently when the assumed encoding is wrong. The rows load, the counts match, and the content is damaged in a way that reconciliation on row counts will never find.

Both of these pass every structural check, which is why they have to be tested for explicitly rather than waited for. A migration test that checks row counts and null rates finds neither.

Replace path by path

With the inventory done, the replacement proceeds incrementally behind the existing system. One path at a time moves to the new implementation while everything else continues to run on the old one.

Three things make this work.

Each path is individually reversible. If a rule was missed, route that path back while it is fixed, with no cutover to unwind.

The old system is still there to answer questions. When new behaviour diverges from old, you can observe the old one rather than reconstructing what it might have done.

Value arrives continuously, so the project can be paused, re-prioritised or stopped with everything delivered so far still working.

The cost is that two implementations coexist for the duration, with a routing seam between them. That is real and it is designed rather than denied.

One rewrite, one cutover
A missed rule surfaces
in production
found by a user, after the old system is gone
Falling back
means discarding new data
Each discovery arrives
as an incident
Value delivered
nothing until everything
no partial value and no natural point to stop
Path by path, behind the old system
A missed rule surfaces
on one path
route it back while it is fixed, with no cutover to unwind
The old system
still answers questions
observe it rather than reconstruct what it might have done
Each discovery arrives
as a finding
Value delivered
continuously
the project can be paused or stopped with everything so far still working
the same rules are missed either way
The cost is two implementations coexisting with a routing seam between them. That is designed rather than denied.
FigureBoth routes miss rules, because an inventory finds only the rules its data exercises. The difference is what a missed rule costs when it surfaces. One route finds it in production with the old system gone and however much new data to unwind, the other finds it on a path that can be routed back while the old system is still there to answer what the correct behaviour was.

Integrations are at-least-once too

One more thing that belongs in the first gate, because it is cheap to handle at design time and expensive later.

Inbound webhooks from messaging and social platforms are typically at-least-once, with retries over an extended window and no ability to request message history. If the intake is not idempotent, every retry is a duplicate record. And because there is no replay, anything genuinely missed during an outage is gone rather than recoverable.

That combination, duplicates on one side and unrecoverable gaps on the other, means the intake has to deduplicate on a stable identifier and record what it received, from the first day rather than after the first incident.

The honest limit

An inventory finds the rules it can reach. A rule that only fires under a condition absent from the copied data is not discovered by inspection, and no amount of thoroughness guarantees completeness.

That is precisely why path-by-path replacement matters more than the catalogue's completeness. The inventory reduces how much you discover in production. Keeping the old system present during replacement is what makes the remainder survivable. Neither alone is sufficient, and most failed migrations had one of the two.

FAQ

Why is replacing a legacy system a migration rather than a build? Because the specification is the existing system. Years of business rules live in stored procedures, triggers, constraints and views, written by people who have left, and no document describes them. A replacement built from the visible behaviour reproduces what users remember rather than what the system enforces.

What is the first deliverable in a legacy replacement? An inventory of what the database enforces on its own, produced against a copy with nothing live touched. It delivers no features, it produces a written description of rules the organisation has never had, and its most useful output is the list of rules with no owner in the new design.

Why do the major vendors recommend incremental replacement over a rewrite? Because a rewrite delivers nothing until it delivers everything, so there is no partial value and no point at which it can be stopped. Every missed rule is found in production after the old system is gone, with no fallback that does not mean discarding new data.

What migration errors load without raising anything? Locale traps. Dates stored in a non-Gregorian calendar read as Gregorian shift by a fixed offset that can be centuries, and text moving through the wrong code page corrupts silently. Both pass structural checks and row-count reconciliation, so they must be tested for explicitly.

How do I handle inbound platform webhooks during a migration? Deduplicate on a stable identifier from the first day, because delivery is at-least-once with retries over an extended window. There is also usually no replay available, so anything missed during an outage is unrecoverable, which means recording what was received matters as much as handling duplicates.

What does path-by-path replacement cost? Two implementations coexisting for the duration of the migration, with a routing seam between them. That is a genuine cost, and it buys individually reversible steps, the old system still available to answer behavioural questions, and continuous delivery of value rather than a single cutover date.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program