Why a successful run proves very little
Most migration programmes track progress in terms of what has been built and deployed: tables created, notebooks converted, pipelines scheduled. Those are useful delivery metrics, but none of them says anything about correctness. A notebook with a join on the wrong key executes successfully. So does a pipeline that silently drops rows with a null in a filter column, or one that loads yesterday’s snapshot instead of today’s.
The same applies to automated code conversion. A figure such as “95% of the code converted automatically” describes how much syntax was translated. It does not describe how much of the workload has been shown to behave correctly, and the remaining 5% is often where the business rules that matter most are hidden. The only metric that matters for cutover is how much of the estate has been demonstrated to produce the intended result.
Validation therefore has to compare outputs, not just observe execution. The rest of this page describes how we structure that comparison.
Compare three systems, not two
The obvious comparison is legacy against target. It is necessary but not sufficient. When the two disagree, a two-way comparison cannot tell you which one is right. Legacy warehouses accumulate history, apply filters nobody remembers and derive columns that no longer match the source. Sometimes the new implementation is the correct one.
Bringing the operational source into the comparison changes that. With source, legacy and target side by side, every business key falls into one of seven regions, and each region points to a different explanation.
| Region | Key present in | What it usually means |
|---|---|---|
| 7 | Source, legacy and target | Expected. The keys match; attribute-level comparison comes next. |
| 4 | Source and legacy, not target | The new pipeline drops records the legacy loads: a filter, join or load gap. |
| 5 | Source and target, not legacy | The target loads records the legacy excluded. Either a legacy filter was not reproduced, or a legacy defect has been corrected. |
| 6 | Legacy and target, not source | Records no longer in the source: history the warehouse keeps, or deletions at source. Check the retention rules. |
| 2 | Legacy only | Historical accumulation or records the legacy derives itself. Usually needs a business decision. |
| 3 | Target only | Keys that should not exist: a faulty join, key generation or duplication in the new pipeline. |
| 1 | Source only | Neither system loads these. Typically an agreed scope filter, or data that arrived after the cut-off. |
Summing all of this into a single “row count difference” hides the information. A table can have identical row counts in legacy and target while hundreds of keys sit in regions 3 and 4, cancelling each other out.
A practical detail that causes more false alarms than any defect: the three systems must be compared at the same cut-off. Comparing a target loaded this morning with a legacy snapshot from last night produces differences that are nobody’s fault. Aligned extraction points, or captured snapshots with a visible timestamp, are part of the validation design.
Levels of validation
Validation is not one check but a stack of them. Each level catches problems the previous one cannot see.
- Structural. Tables, columns, data types and nullability exist as expected. Cheap, and catches missing columns and silent type changes, such as a decimal that became a float.
- Row counts. Useful as a first signal, misleading as a conclusion, for the reason described above.
- Key coverage. The seven-region comparison on business keys, including duplicate keys in any of the three systems.
- Column-level comparison. For keys present in both legacy and target, which attribute values differ, and for how many keys. Null distributions and distinct-value counts per column reveal problems in columns that are rarely inspected.
- Aggregates. Sums, averages and counts by meaningful dimensions such as period, entity or product. Numeric tolerances belong here: distributed engines can sum floating-point values in a different order and produce differences in the last decimals. Such differences should be allowed explicitly with an agreed tolerance, not ignored implicitly.
- Business rules. Invariants the business states in its own terms: every contract has exactly one active owner, revenue equals the sum of its lines, no order predates its customer.
- History. For slowly changing dimensions: one current record per business key, no overlapping or gapped validity periods, consistent effective dates and current flags, and unknown or default members present where facts expect them. SCD2 behaviour is one of the most frequent sources of subtle regressions, because a pipeline can be correct for today’s data and wrong for history.
- Downstream outputs. The figures in the reports and semantic models that people actually use, reconciled against the legacy equivalents. This is the level business owners care about, and the one most often skipped.
For data warehouses, the fact-to-dimension relationships deserve their own checks: facts whose dimension keys resolve to the unknown member, surrogate keys that differ between legacy and target but must still join consistently, and the behaviour of late-arriving dimensions.
Classify differences instead of failing them
A migration with no differences at all is rare, and not always desirable. The useful output of validation is therefore not a red or green flag but a classification of every difference found.
| Classification | Meaning | Typical action |
|---|---|---|
| Exact match | Values identical in legacy and target | None |
| Within tolerance | Numeric difference inside an agreed threshold | Record the tolerance and why it applies |
| Expected architectural change | Caused by a deliberate design decision, such as regenerated surrogate keys, trimmed strings or timestamps normalised to UTC | Document once, approve once |
| Known source-quality issue | Legacy and target handle poor source data differently | Agree a rule with the data owner |
| Suspected regression | Target deviates without an explanation | Fix before the table is promoted |
| Business review required | Both behaviours are defensible and the intent is unclear | Decision by the business owner, recorded |
Classification turns a list of failures into a manageable backlog. It also stops the same accepted difference from being investigated again in every test cycle.
Validate according to risk
Not every table deserves the same scrutiny. Financial, regulatory and customer-facing outputs justify full column-level comparison, aggregate reconciliation and business sign-off. Shared dimensions deserve thorough history checks, because every downstream fact depends on them. Short-lived intermediate datasets often need no more than structural checks and key coverage, as long as the outputs built on them are validated properly.
Setting these tiers explicitly at the start makes the validation effort predictable, and makes it possible to explain to an auditor why a given table was tested the way it was.
Evidence a business owner can review
Test results written for engineers rarely help a finance controller decide whether a migrated report can be trusted. An assertion failure in a Spark log is not a basis for sign-off.
The overlap view above works well with business stakeholders because it answers questions they can reason about: which records exist in both systems, which exist only in the old one, which only in the new one, and for which records the values differ. A controller who sees that the keys only in legacy all predate a known policy change can close the question in a meeting. The same finding in a log file would take days to escalate.
The principle: technical validation should produce evidence that the person accountable for the data can review and accept.
Parallel running
Running legacy and target side by side for a period, and comparing their outputs after every load, catches problems that one-off testing misses: incremental logic that only fails on specific days, month-end processing, late-arriving data, and drift that builds up over several runs.
It is a risk-based technique, not a rule. A reporting mart rebuilt from scratch every night may need a few comparison cycles. A warehouse with complex incremental and historical logic may need several month-end closes. What matters is that the exit criteria are agreed in advance, for example a number of consecutive runs without unexplained differences in the highest-risk tables, so that the end of the parallel run is a decision rather than a negotiation.
Leave evidence behind with every release
Each release should leave a record that answers, months later: what was tested, against which run and code version, which differences were found, which were accepted and by whom, and which remain open. When the same checks with the same thresholds run on every cycle, a defect confirmed fixed has been confirmed with the same method that found it.
This evidence is also what turns continuous deployment into safe deployment. A pipeline that deploys successfully can still remove a column a report depends on, or change the grain of a table. Deployment automation that knows which consumers are affected by a change, and whether validation has passed, can require explicit approval for breaking changes instead of applying them silently. Automated deployment should not mean automated destruction.
Our Migration Testing Workbench is built around these principles. The approach itself does not depend on any particular tool.
Questions we are often asked
Is comparing row counts enough to validate a migration?
No. Row counts can match while different records are present in each system, because missing and extra keys cancel each other out. Key coverage, column-level comparison and aggregates are needed to show that the same records carry the same values.
Why include the source system in migration testing?
When legacy and target disagree, the source shows which one reflects reality. It separates target defects from legacy history, legacy filters and legacy defects, which otherwise all look like the same row-count gap.
How long should legacy and target run in parallel?
It depends on the risk and the processing calendar. Nightly full rebuilds may need a few cycles; warehouses with incremental and historical logic often need to pass through at least one month-end. Agree the exit criteria before the parallel run starts.
When should migration validation start?
As soon as the first migrated table produces data, not in a test phase just before cutover. Differences found in the sprint in which the code was written are far cheaper to explain and fix.