The Tuesday three weeks later
Cutover weekends are usually fine. The new system comes up on Sunday night, people log in on Monday, the screens load, and the customer list looks about the right length. Somebody sends a message with a champagne emoji.
The trouble tends to arrive on a Tuesday three weeks later. Finance runs the quarter-end report and the revenue figure is off by a little under two percent. It takes a day to find out why: a few hundred orders were placed by customers whose records were marked as “type 4” in the old system, a value nobody remembered existed, and the migration script quietly skipped them. The orders came across. The customers they pointed at did not. The screens never complained, because a screen only shows what exists.
We’ve watched some version of this story play out more often than we’d like, and it is why data is the part of a modernization project that people are right to be nervous about. A new app can be clicked through and judged in an afternoon. Ten years of records cannot. They get judged slowly, by the business, one surprised colleague at a time.
It is three jobs pretending to be one
Most migrations that go wrong were planned as a single task called “move the data.” It is three tasks, with different risks and different people who can tell you whether they were done right.
Reading it out. The old system runs on SQL Server, or Access, or a SaaS product with an export button. Getting rows out is an engine problem: types, encodings, date formats, a column that is text in one table and a number in the next. Tedious, well understood, and mostly solvable by machines.
Deciding what becomes what. The old tblCustomer is not the new customers table. One old record might become a customer and an address. A status column with eleven values might need to become six. CustType = 3 might mean “internal”, but only the person who ran the warehouse in 2016 knows that for sure. This is the real job, and no tool can do it alone, because the answer depends on what the business means by its own data.
Putting it in safely. Back up the target, load tables in an order that keeps references intact, and check what landed before anyone depends on it.
AWS and plenty of other vendors will happily do the first job for you at any scale. The trouble is that a tool which only does the first job delivers the old database’s shape into a new engine and stops, at exactly the point where the risk begins. So in Sapilon’s data migration we keep a hard seam between the three and put most of the product into the middle one.
Every assumption on a card
When you ask for a proposed mapping, your project’s agent reads the old tables you chose to bring and the new schema it helped design, and writes one card per new table. A card holds decisions and nothing else: where the rows come from, what identifies a record in the old system, which old columns are not carried, and every assumption it had to make, in plain words. “CustType 1, 2 and 3 read as retail, trade and internal.”
You go through each card line by line and either accept a line or disagree with it and say what is wrong. The agent reworks what you disagreed with and the changed lines come back for another look. A table counts as confirmed only when every line on it has been accepted by a person.
This sounds slower than letting a model write a migration script, and for about an hour it is. What it buys is that the type-4 customers from the opening story would have shown up as a line saying “values other than 1, 2 and 3 are not mapped”, sitting in front of someone who could say “wait, what about 4?” The cheapest place to catch a wrong assumption is before it becomes code.
Leaving data behind is treated as a decision too. Nine years of audit log that nobody reads can stay where it is, but the reason gets written on its own card. Two years from now, when an auditor asks where the old log went, the answer will be findable.
Rehearse it until nobody is nervous
Once the mapping is confirmed, you run it into your Rehearsal Server: a real copy of the production setup, holding nothing anyone depends on yet. Then you run it again. Each run reads the old data fresh, loads it through the current mapping, replaces whatever the previous run loaded, and ends with a report.
We spent a long time on the order of that report, because it gets read by the person who has to sign off, not the person who wrote the SQL. It opens with a few real records shown before and after, since finding a customer you know and looking at them is the check most people trust. Then record counts, with a reason for everything left out. Then totals: the old sum of every money and quantity column next to the new one. A line like “Orders 2019–2025: €4,182,336.20 in both” ends more arguments than any spreadsheet we’ve seen.
Then come the two checks that catch the Tuesday problem. Any record pointing at something that did not come across fails the run outright. And every distinct value of every status or type column is listed next to what it became, so a code that turned into nothing stands out on the page instead of hiding in a join.
The first rehearsal almost always finds something. So does the second. By the fifth or sixth the report is uneventful, and that is the goal. We explain the reasoning behind a separate rehearsal stage at more length in Design, Build, Rehearsal, Live; for data it matters more than anywhere else, because data mistakes compound with every day real users spend on top of them.
One rule for the Live Server
The product enforces a single rule at cutover: the Live Server only runs a migration that has already rehearsed cleanly with exactly the same mapping. Change one line after your last clean rehearsal and you rehearse again before going Live. No override, no “it’s only a small change.”
Two other properties make the rule livable. Every record carries its old identifier, so a second run updates the records the first one made instead of duplicating them, and a mapping fix becomes a correction rather than a clean-up project. The same crosswalk means “which old record did this come from?” still has an answer long after the old server is switched off.
And the cutover is not run from some separate migration tool. It happens inside the Go Live step, next to the decisions that belong with it: how the old system will be frozen (read-only overnight is enough for most businesses), what happens to old accounts (usually an invitation to set a new password), and how long the old system stays reachable, with a named person who decides whether to roll back.
When to bring in a person
Most migrations we see are an export from a business system, a handful of spreadsheets, or a database the size of a few years of orders. Those are well within reach of the person who knows what the data means, working through the steps with the agent.
Some are not. A source that cannot be frozen, a schema with two hundred tables and no documentation, a compliance requirement on how customer data is handled in transit. For those, the first step on the migration page offers to bring in a verified expert who reads SQL and has seen what old data tends to hide. They quote before they start, and they work on the same cards and the same reports you do, so their judgement ends up written down in the project rather than in someone’s head.
Treat your data like the asset it is
The files an old system leaves behind are, as we argued in your old system already wrote the spec, the most accurate requirements a rebuild will ever get. The records are something more than that. They are the business: every customer who ever ordered, every invoice that was paid, the history that makes this year’s numbers comparable to last year’s.
A migration plan worth trusting treats them that way. It writes its assumptions down where a person can argue with them, practises on a copy until the practice gets dull, and refuses to touch the real thing until it has. Nobody should have to find out on a Tuesday.