Skip to content

Data migration acceptance checklist

Twelve steps. The first five happen before anyone migrates anything.

Free · complete · nothing held back

This is the procedure from the Migration Acceptance Kit, published in full. It is useful with no software at all — most of it is decisions, not tooling. If it is useful to you, the kit automates step 7 and makes step 10 possible; if it is not, you have lost nothing reading it.

It is written for the person who has to sign the migration off, which is often not the person who ran it.

Before the migration

1. Name the source of truth, and freeze it

Write down the exact export that is authoritative: which system, which query or export job, which timestamp, which file. Take its SHA-256 and record it alongside.

shasum -a 256 export-2026-09-11.csv

If the source system is still accepting writes while you migrate, you are not reconciling against a fixed thing, and every count you produce will be arguable. Either freeze it, or record the cutover moment and accept that rows created after it are a separate, smaller reconciliation.

2. Nominate the key, and prove it is actually a key

Which column, or combination of columns, identifies a row in the source and survives into the target?

Then check that it is unique before you rely on it. The cheapest version of this check is to compare the source against itself and count the duplicates. A non-zero duplicate count means your key is not a key, and everything downstream is built on it.

3. Decide what "the same" means, field by field

This is the step people skip, and it is the step that causes the argument at the end. For every field you care about, write the rule down:

  • Is   Ada Lovelace  the same as Ada Lovelace? (whitespace)
  • Is ADA@EXAMPLE.COM the same as ada@example.com? (case)
  • Is 120.50 the same as 12050? (major versus minor units)
  • Is 01/04/2024 the same as 2024-04-01? Say which order. This one has a right answer per source system and no universal default.
  • Is a blank the same as zero? Almost never.
  • Is Y the same as true? Usually — but write it down.

Whatever you use to compare, these rules should live in a file rather than in someone's head, and the result should carry a reference to the rules that produced it. A number without its rules cannot be checked later.

4. Decide, in advance, which rows you are NOT migrating

Every migration drops rows: test accounts, soft-deleted records, rows outside the agreed date range, rows from a region that is not in scope.

Dropping rows is fine. Dropping rows you did not declare is not — at sign-off it is indistinguishable from a bug, and nobody can tell the difference months later.

So write the exclusions as numbered rules, and produce a list: one row per excluded key, with the rule that excluded it.

customer_id,reason C-1005,closed before cutover - excluded by agreed rule R3 C-1006,merged into C-1001 in legacy system - excluded by agreed rule R7

An exclusion with no stated reason is a silent drop wearing a badge. It should be rejected by whatever you use, including by you.

5. Agree the pass condition before you can be tempted

State what result you will accept while you still have no idea what the result will be. For example:

The migration is accepted when every source row is matched or declared excluded, and every target row is attributed to exactly one source row, with no exceptions.

If you are going to tolerate something, name it now and say why — for instance that a last-login timestamp cannot be verified because the legacy system did not record it before 2019, and the new system cannot invent it.

Deciding this afterwards, with a deadline overhead, is how "we'll fix the 1,400 missing rows later" gets into a sign-off.

During and after

6. Export both sides the same way

Same delimiter, same encoding, same quoting, same null convention. Where you cannot, declare the difference rather than fixing it by hand. A hand-fixed file cannot be regenerated, and an export you cannot regenerate is not evidence.

7. Account for every row on both sides

Not a row count. Every source row should land in exactly one category, and so should every target row, and the category totals should equal the raw row counts read from each file. At minimum, distinguish:

  • matched — present and equal under your declared rules
  • changed — present, but something differs
  • unverified — present, but a field you cannot check either way
  • missing — gone, and not declared
  • excluded — declared, with a reason
  • excluded but present — you said you would drop it; it is there
  • duplicated — on either side
  • orphan — in the target, in no source. Something invented this

The last three are the ones that matter most and the ones almost nobody checks. A row count is blind to all of them, and it is blind in the most flattering direction: an orphan and a missing row cancel out perfectly.

8. Work the findings in this order

  1. Orphans. Rows in the target that are in no source. Usually a partially applied re-run, a test fixture that shipped, or a join that fanned out. Always worth understanding before anything else.
  2. Excluded but present. Either the exclusion did not apply, or the rule is wrong.
  3. Missing. The classic. Rows that went nowhere and were not declared.
  4. Duplicates and ambiguity. Your key is not behaving as a key on one side. Resolve this before trusting any other count.
  5. Changed. Some will be intended transformations you forgot to write down. Fix the written rule, then re-run.
  6. Unverified. Fields you cannot confirm either way. These do not become matched by wishing. Get the data, or accept them explicitly and in writing.

9. Re-run until it matches the pass condition

Each iteration changes either the data or the rules. Changing the rules is legitimate — you are encoding something you had not written down — but it is a change to the agreement, so it belongs in the record with a reason.

10. Keep something anyone can re-run

Keep the report, the rules, and the SHA-256 of both input files. Together they should let someone re-derive the identical result offline, months later, with no access to either system.

This is the difference between evidence and an assertion. If your result cannot be reproduced without the original systems, then switching off the old system destroys your ability to defend the migration.

11. Write the sign-off, including what you did not check

Say what was checked, what was not checked, and what was tolerated. The limits are the most valuable part of the memo: they are what protects you when someone later finds a problem in a field nobody compared.

A sign-off that implies everything was verified, when eleven of forty columns were, is the document that causes the real argument.

12. Leave it running, or leave it runnable

If both systems keep running in parallel, schedule the same check. Divergence then shows up as a failing job rather than as a customer complaint.

If the old system is being switched off, archive the export files together with whatever produced the report. The ability to re-derive the number is the thing you are actually keeping.

The short version

Agree the key, the field rules and the exclusions before you migrate. Account for every row on both sides. Publish the limits with the number. Keep something anyone can re-run.

If you want the tool

The Migration Acceptance Kit makes step 7 one command and step 10 a byte-stable report with a content hash, and ships the sign-off memo for step 11. It is one standard-library Python file with 74 tests, a perpetual licence and full source, at US$240 for a single organisation or US$600 for consultancy use on client work.

Everything above stays here, free, regardless. It is the part that is worth having whether or not you buy anything.