Four Billion Rows, Twenty years of Debt : A Migration Story

Four Billion Rows, Twenty Years of Debt: A Migration Story *{box-sizing:border-box;margin:0;padding:0} body{background:#f9f8f5;font-family:’Source Serif 4′,Georgia,serif;color:#1a1a18} .page{max-width:840px;margin:0 auto;padding:3rem 2rem 5rem} /* Hero band */ .hero-band{background:linear-gradient(135deg,#0B1F3A 0%,#183660 60%,#1a4a80 100%);border-radius:18px;padding:3rem 2.5rem 2.5rem;margin-bottom:2.5rem;position:relative;overflow:hidden} .hero-band::before{content:”;position:absolute;top:-40px;right:-60px;width:320px;height:320px;background:radial-gradient(circle,rgba(255,255,255,.04) 0%,transparent 70%);border-radius:50%} .hero-band::after{content:”;position:absolute;bottom:-60px;left:-30px;width:240px;height:240px;background:radial-gradient(circle,rgba(24,95,165,.3) 0%,transparent 70%);border-radius:50%} .tag{display:inline-block;font-family:’JetBrains Mono’,monospace;font-size:11px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:#7DC4FF;background:rgba(125,196,255,.12);border:1px solid rgba(125,196,255,.25);border-radius:4px;padding:3px 10px;margin-bottom:1.25rem} h1.blog-title{font-family:’Playfair Display’,Georgia,serif;font-size:clamp(26px,4.5vw,46px);font-weight:700;line-height:1.13;color:#fff;margin:0 0 1.1rem;letter-spacing:-.02em} h1.blog-title em{font-style:italic;color:#7DC4FF} .subtitle{font-size:17px;font-weight:300;font-style:italic;color:rgba(255,255,255,.7);line-height:1.7;margin:0 0 1.75rem;border-left:3px solid #4A9EE0;padding-left:1.1rem} .meta{display:flex;gap:18px;flex-wrap:wrap;font-family:’JetBrains Mono’,monospace;font-size:11.5px;color:rgba(255,255,255,.45)} /* Body text */ .body-text{font-size:17px;line-height:1.87;color:#2a2a28} .body-text p{margin:0 0 1.5em} h2.section-title{font-family:’Playfair Display’,Georgia,serif;font-size:27px;font-weight:700;color:#111;margin:3rem 0 .75rem;line-height:1.2} h2.section-title .accent{color:#185FA5} /* Stats */ .stat-row{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin:2.5rem 0} .stat-box{background:#fff;border:1px solid #e0ded6;border-radius:12px;padding:1.1rem .8rem;text-align:center;position:relative;overflow:hidden} .stat-box::before{content:”;position:absolute;top:0;left:0;right:0;height:3px} .stat-box.blue::before{background:#185FA5} .stat-box.green::before{background:#1D9E75} .stat-box.gold::before{background:#BA7517} .stat-box.purple::before{background:#534AB7} .stat-box.red::before{background:#C04040} .stat-num{font-family:’Playfair Display’,Georgia,serif;font-size:30px;color:#111;line-height:1} .stat-lbl{font-family:’JetBrains Mono’,monospace;font-size:10px;color:#888;text-transform:uppercase;letter-spacing:.08em;margin-top:6px} /* Phase cards */ .phase-card{border:1px solid #e0ded6;border-radius:14px;padding:1.4rem 1.6rem;margin:1.5rem 0;background:#fff} .phase-num{font-family:’JetBrains Mono’,monospace;font-size:11px;color:#185FA5;background:#E6F1FB;padding:2px 9px;border-radius:3px;display:inline-block;margin-bottom:10px} .phase-title{font-family:’Playfair Display’,Georgia,serif;font-size:21px;font-weight:700;color:#111;margin:0 0 10px} .phase-body{font-size:15.5px;line-height:1.8;color:#555} /* Highlight card — new dark */ .highlight-card{background:linear-gradient(135deg,#0B1F3A,#183660);border-radius:14px;padding:1.6rem 2rem;margin:2rem 0;color:#fff} .highlight-card .label{font-family:’JetBrains Mono’,monospace;font-size:10px;color:#7DC4FF;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.6rem} .highlight-card p{font-size:16px;line-height:1.75;color:rgba(255,255,255,.8)} .highlight-card strong{color:#fff} /* Callout */ .callout{border-left:4px solid #BA7517;padding:1rem 1.4rem;margin:2.25rem 0;background:#FAEEDA;border-radius:0 10px 10px 0} .callout p{font-size:16px;line-height:1.75;color:#633806;font-style:italic} .callout-blue{border-left:4px solid #185FA5;padding:1rem 1.4rem;margin:2.25rem 0;background:#E6F1FB;border-radius:0 10px 10px 0} .callout-blue p{font-size:16px;line-height:1.75;color:#0B2E5A;font-style:italic} /* Diagram wrapper */ .diagram-wrap{margin:2.25rem 0;border:1px solid #e0ded6;border-radius:14px;overflow:hidden;background:#fff} .diagram-inner{padding:1.5rem 1rem} .diagram-caption{font-family:’JetBrains Mono’,monospace;font-size:11px;color:#888;text-align:center;padding:9px 0 11px;background:#f5f4ef;letter-spacing:.04em;border-top:1px solid #e0ded6} /* Inline badges */ .badge{display:inline-block;font-family:’JetBrains Mono’,monospace;font-size:11px;background:#E6F1FB;color:#185FA5;border-radius:4px;padding:1px 7px;vertical-align:middle;margin:0 2px} .badge-gold{background:#FAEEDA;color:#854F0B} .badge-green{background:#E1F5EE;color:#0F6E56} .badge-red{background:#FCEBEB;color:#A32D2D} /* Key insight blocks */ .insight-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:1.75rem 0} .insight-item{background:#fff;border:1px solid #e0ded6;border-radius:10px;padding:1.1rem 1.2rem} .insight-icon{font-size:22px;margin-bottom:.5rem} .insight-title{font-family:’Playfair Display’,Georgia,serif;font-size:15px;font-weight:700;color:#111;margin-bottom:.4rem} .insight-body{font-size:14px;line-height:1.7;color:#666} .divider{border:none;border-top:1px solid #e0ded6;margin:3rem 0} /* Pull quote */ .pull-quote{font-family:’Playfair Display’,Georgia,serif;font-size:clamp(18px,2.5vw,24px);font-style:italic;color:#185FA5;border-top:2px solid #185FA5;border-bottom:2px solid #185FA5;padding:1.2rem 0;margin:2.5rem 0;line-height:1.5;text-align:center} /* Timeline badge row */ .timeline-badges{display:flex;gap:8px;flex-wrap:wrap;margin:.75rem 0 1.25rem} .tbadge{font-family:’JetBrains Mono’,monospace;font-size:11px;padding:4px 11px;border-radius:20px;border:1px solid} .tbadge-b{background:#E6F1FB;color:#185FA5;border-color:#B5D4F4} .tbadge-g{background:#E1F5EE;color:#0F6E56;border-color:#5DCAA5} .tbadge-o{background:#FAEEDA;color:#854F0B;border-color:#FAC775} .tbadge-r{background:#FCEBEB;color:#A32D2D;border-color:#F09595} @media(max-width:620px){ .stat-row{grid-template-columns:1fr 1fr} .insight-grid{grid-template-columns:1fr} .page{padding:1.5rem 1rem 3rem} .hero-band{padding:2rem 1.4rem} }
Data Engineering · Cloud Migration

Four Billion Rows,
Twenty Years of Debt:
A Migration Story

How we moved two decades of business-critical data — 4 billion rows — from an IBM DB2 mainframe to Azure across subscription boundaries, through a medallion architecture, without losing a single record.

⏱ 18 min read ☁ Azure · DB2 · Qlik Replicate 📅 Eight phases · Four dry runs · One very long cutover weekend

Some data migrations are routine. Export a table, import a table, done. This was not that.

The source system had been running for over two decades. It had been patched, extended, reimplemented, and patched again. Entire business units had been built on top of it. And sitting inside an IBM DB2 mainframe at the vendor’s data centre were four billion rows of business-critical records — master data, transactional history, operational reference data, and process audit logs — accumulated since the early 2000s.

What we tackled was a full-scale migration off that mainframe — through a vendor’s Azure subscription, across a private network boundary, through a purpose-built medallion lake, and ultimately into our production Azure Hyperscale database. Eight phases, four dry runs, a load that initially took four days, a parallel run to catch what tests alone cannot, and one very detailed run book. Here’s how we did it.


4B
Rows Migrated
20+
Years of Data
8
Phases
4
Dry Runs
3
Medallion Layers
📊 Scale Reality Check

Four billion rows is not an abstract number. At a modest average row size, that’s terabytes of structured data carrying over two decades of business logic baked into its values, its nulls, and its edge cases. Every design decision — parallelism strategy, validation coverage, dead-letter routing — needed to be made at this scale from day one.


The Architecture at a Glance

Before diving into the phases, here is the full data journey — from the mainframe to the hands of business consumers. The cross-subscription boundary was one of the more interesting challenges: a Private Endpoint established a secure, private network connection between our Azure subscription and the vendor’s — no public internet traversal.

Source — Vendor managed IBM DB2 Mainframe · 20+ yrs Qlik Replicate CDC streaming Vendor Azure SQL Vendor subscription 🔒 Private Endpoint Our Azure Subscription Bronze Layer Raw landing zone Silver Layer Transform & cleanse Gold Layer Consumption-ready Azure Hyperscale Serving DB Consumers Applications BI & Reporting Data Science
Fig 1 — End-to-end data flow: from IBM DB2 mainframe through to Azure Hyperscale

The Eight-Phase Roadmap

We structured the migration into eight distinct phases — from understanding the source to verifying the result. Each phase had clear entry and exit criteria before the next could begin.

1 Discovery Source analysis Understand 20+ yr data model & flows 2 Data Modelling Schema rationalisation Reduce tables & drop unused attributes 3 Medallion Build Bronze → Silver → Gold Land, transform & serve 4B rows 4 Dry Runs ×4 End-to-end rehearsals Optimise & time each pipeline step 5 Data Testing Source vs target rules Domain-level data quality validation 6 Prod Cutover Run book execution Go/no-go checkpoints & rollback plan 7 Failed Records Dead-letter routing Isolate failures, keep pipeline moving 8 Reconciliation Final sign-off report Count, validate & attest 4B rows end-to-end
Fig 2 — Migration phases from discovery through to reconciliation

Phase 1 — Discovery

PHASE 01
Understanding Twenty Years of Accumulated Decisions

Before a single byte moves, you need to understand the data deeply — not just its schema, but its meaning. A system that has been running for two decades carries more than data: it carries the scar tissue of every business decision, every workaround, every field repurposed for a use it was never designed for. We ran workshops with the vendor’s subject matter experts to map business processes end-to-end, distinguishing data that drove live business logic from orphaned reference data that had simply accumulated since the early 2000s. Several tables had not been written to in over eight years.

Phase 2 — Data Modelling

PHASE 02
Rethinking the Model, Not Just Copying It

Legacy mainframe schemas carry decades of design decisions. Rather than a 1:1 lift-and-shift, we designed a new data model that matched how the business actually works today — consolidating overlapping tables and dropping attributes that had never been populated in production. Two decades of accumulated schema drift meant many columns were structurally present but semantically dead.

“Resist the urge to faithfully replicate every legacy design choice. The migration is your opportunity to fix what was never quite right the first time — and with twenty years of drift, there was plenty to fix.”

Phase 3 — Medallion Architecture

The three-layer medallion pattern was central to our approach. Each layer has a distinct contract — and violating those contracts is how pipelines become unmaintainable at scale.

Source Vendor Azure SQL Bronze Raw as-is copy No transforms Full fidelity Reprocess safety net Narrow scope Silver Cleansed & typed Deduplication Business rules applied Referential integrity Domain-agnostic source of truth Widest scope Gold Domain-modelled Aggregated Query-optimised Shaped for consumers → Azure Hyperscale DB Apps / BI / Science Consumer scope
Fig 3 — Medallion layers: Bronze lands raw, Silver cleans, Gold serves
PHASE 03
Bronze, Silver, Gold — and Why Each Layer Matters at Billion-Row Scale

Bronze lands everything raw — your safety net if anything goes wrong downstream. At four billion rows, that safety net is not optional: when transformation logic changed mid-project (and it did), we re-derived Silver and Gold from the same Bronze data without going back to the source. Silver is where transformations, deduplication, and business rules are applied: clean, semantically correct, domain-agnostic. Gold shapes that clean data for specific consumers and feeds directly into Azure Hyperscale DB as the serving layer.


The Load That Took Four Days — and How We Fixed It

Our first end-to-end dry run completed the initial full load in just over four days. For a production cutover with a target maintenance window measured in hours, that was clearly not acceptable. The bottleneck analysis was humbling — large tables were being loaded sequentially, transformation pipelines were not exploiting available concurrency, and network throughput to the Private Endpoint was being consumed by a single thread.

BEFORE — Sequential Load (4 days) Table A Table B Table C Table D Table E Table N… ≈ 4 days AFTER — Parallelised Load (optimised) Lane 1 Large Tables Partition 1 Partition 2 Partition N Lane 2 Mid Tables Batch A Batch B Lane 3 Ref Data All tables Load Complete Hours not days KEY TECHNIQUES Key-range partitioning Parallel ADF lanes Index disable & rebuild Dependency sequencing Bulk insert tuning
Fig 4 — From sequential to parallel: transforming a 4-day load into a cutover-viable window
LOAD OPTIMISATION
From Four Days to a Cutover-Viable Window

The parallelisation strategy had four pillars. First, index management: all non-clustered indexes were dropped on every target table before the load began and rebuilt after completion — eliminating the overhead of maintaining index integrity on every row insert across four billion records, which alone delivered a substantial reduction in load time. Second, table partitioning: large tables were split into key-range partitions and loaded concurrently across independent ADF pipeline threads. Third, independent pipeline lanes: tables with no foreign-key dependencies on each other were grouped into parallel ADF lanes, so reference data, large transaction tables, and mid-tier tables loaded simultaneously. Fourth, dependency-aware sequencing: tables with dependencies were sequenced so parent tables completed before child tables started — maintaining referential integrity without serialising the entire load.

🔀
Partition Large Tables
Tables with billions of rows were split by primary key range or date partition, allowing multiple readers and writers to operate simultaneously without lock contention.
⚡
Index Disable & Rebuild
All non-clustered indexes were dropped before the load and rebuilt after. Maintaining index structures on every insert across 4 billion rows adds enormous overhead — removing that constraint was one of the single biggest wins.
🔗
Dependency Mapping
We produced a full dependency graph of all tables before optimising. Any table that could run in parallel safely was moved to a parallel lane — only true dependencies remained sequential.
📡
Network Saturation Avoided
Parallelism was capped to prevent Private Endpoint saturation — we found the optimal lane count experimentally across dry runs, balancing throughput against network contention.

The Parallel Run — Catching What Tests Alone Cannot

Data quality tests and dry runs are powerful, but they operate on snapshots. Real production traffic is different: it produces edge cases, timing windows, and data combinations that no test suite fully anticipates. Before we committed to cutover, we ran a parallel period — operating both the legacy mainframe and the new Azure platform simultaneously, with live production data flowing through both and outputs compared continuously.

Live Traffic Production events Legacy System IBM DB2 Mainframe Output A Legacy result New Platform Azure Hyperscale Output B New result Comparator A = B? Log diff ✓ Match → confidence +1 ✗ Diverge → investigate Both systems run simultaneously under live load during the parallel period
Fig 5 — Parallel run architecture: live traffic through both systems, outputs compared continuously
PARALLEL RUN
Running Both Systems Simultaneously to Surface Hidden Issues

The parallel run is where theory meets reality. Both the legacy IBM DB2 system and the new Azure platform received the same live production data and their outputs were compared record-by-record. Discrepancies fell into three categories: known differences (intentional rationalisation we’d modelled), data quality issues in the legacy system that were newly visible now that we had a clean target to compare against, and a small category of genuine transformation bugs — logic errors that no dry run had surfaced because the specific combination of values simply hadn’t appeared in any test dataset. The parallel run found all of them.

“The parallel run found three transformation bugs that four dry runs and thousands of validation rules had not. Twenty years of production data contains value combinations you will never think to test for. Run both systems together and let the comparison do the work.”

Week 1–2: Baseline comparison Week 3–4: Bug fixes & re-validate Week 5: Divergence rate drops to <0.01% Week 6: Sign-off for cutover

Phase 4 — Dry Runs

Each of the four dry runs was a full end-to-end simulation. With each iteration we identified pipeline bottlenecks, refined sequencing, tightened timing estimates, and evolved the parallelisation approach. By run four, every step had a known duration — which fed directly into the production run book.

Continuous improvement Run 1 4-day load Sequential only Run 2 Parallelism introduced Run 3 Tuned lanes & partitions Run 4 Stable timings Run book ready PROD Cutover Rehearsed. Confident.
Fig 6 — Dry run progression: sequential 4-day load in Run 1 → optimised parallel cutover by Run 4
PHASE 04
Four Dress Rehearsals Before the Real Thing

Run 1 established the baseline — and exposed the four-day load problem. Run 2 introduced the parallelism strategy and, critically, disabled all non-clustered indexes on target tables before loading, which cut the load time dramatically. Run 3 fine-tuned partition counts, ADF lane widths, and dependency sequencing. Run 4 was the final dress rehearsal: stable timings, known durations at every step, and a complete run book. Nothing happened on cutover day that the team hadn’t already done before.

Phase 5 — Data Quality Testing

PHASE 05
Validating Every Business Domain, Not Just Row Counts

Row counts are necessary but not sufficient — particularly across twenty years of accumulated data. We built domain-scoped quality rules: referential integrity, key uniqueness, value ranges, cross-table consistency, and temporal validity checks. Source data was compared against target at every transformation stage to catch records that were present but silently transformed incorrectly. Legacy systems with two-decade histories often contain data that was valid under rules that no longer apply — our test suite needed to distinguish those from actual migration errors.


Phase 6 — Production Cutover

After four dry runs and a successful parallel period, the production cutover was the most rehearsed event on the team’s calendar. But at four billion rows, even a well-rehearsed plan encounters operational reality. Here is how the cutover was structured.

CUTOVER WEEKEND TIMELINE T-4h T-2h T=0 Cutover T+Load complete ✓ Live Pre-Cutover Checklist Sign-offs · Rollback confirmed Team on-call · Go/no-go ready Source Freeze Write-locked Parallel ADF Full Load Indexes disabled · All lanes running · Checkpoints active Validation Sweep Counts · Spot checks · Domain sign-offs Index Rebuild Then go live ✓ Rollback to legacy if needed
Fig 7 — Cutover weekend timeline: freeze → parallel ADF load (indexes disabled) → index rebuild → validate → go live
PHASE 06
Cutover Day: Executing the Run Book With Confidence

The run book captured every step — owners, expected durations (calibrated across four dry runs), go/no-go checkpoints, and an explicit rollback plan. The cutover followed a defined sequence: legacy system write-lock, then the parallel ADF full load with all indexes disabled on the target tables, followed by index rebuild once the load completed, a validation sweep across all business domains, and finally traffic switch-over. Running the ADF pipelines in parallel — with indexes offline — was the combination that made the load window achievable. Three formal go/no-go decision points during the load meant the team was never more than one step from a tested rollback position.

The parallel run had done its job — by the time we hit cutover, the divergence rate between legacy and new outputs was below 0.01%. The load completed ahead of the window estimate. The rollback was never needed.

Phase 7 — Failed Record Handling

Not every record loads cleanly — and designing the pipeline to halt on every error is a recipe for a cutover that never completes. At four billion rows, a halting strategy is simply not viable.

Source Records Load Pipeline Validation rules ✓ Pass Target DB Azure Hyperscale ✗ Fail Dead Letter Isolated staging Business Team Manual review
Fig 8 — Dead-letter routing: failures isolated without halting the main pipeline
PHASE 07
Designing for Failure Without Stopping the Pipeline

Any record that failed during the load was redirected to an isolated staging area rather than blocking the main pipeline. The primary load continued uninterrupted. Failed records were catalogued with their source identifiers, failure reason codes, and the pipeline step at which they failed — and handed to the business team for investigation and manual remediation, with full traceability back to the source record.

Phase 8 — Reconciliation

PHASE 08
Closing the Loop on Four Billion Rows

The migration ends when you can prove it. At four billion rows across twenty years of history, the reconciliation report was not a formality — it was the evidence that the migration had happened correctly. We produced counts by entity, validation pass/fail rates, volumes redirected to the dead-letter store, and a complete attestation that every source record was accounted for end-to-end. The parallel run divergence history was included as supplementary evidence. This became both the sign-off document and the baseline for future data lineage queries.


What We’d Do Differently

Start the parallel run earlier. We ran it after the dry runs as a final validation gate — but running it concurrently with later dry runs would have caught the transformation bugs before run four rather than after it.

Design for parallelism from day one rather than retrofitting it after run one’s four-day result. The dependency graph exercise — mapping which tables could be loaded concurrently — should have been completed during the data modelling phase, not the optimisation phase.

Invest more in the discovery phase than feels necessary. Every hour spent understanding a twenty-year-old system’s accumulated quirks saves three hours of puzzled debugging later. The Bronze layer proved its greatest value mid-project: when transformation logic changed, we re-derived Silver and Gold without going back to the source.

Mainframe-to-cloud migrations at this scale are among the most complex data engineering challenges in the enterprise. Four billion rows. Twenty years of history. One shot at a production cutover. With the right architecture, disciplined process, a healthy respect for edge cases — and a parallel run that refuses to let hidden issues stay hidden — they are entirely tractable.

“Four billion rows doesn’t arrive all at once. It arrives one decade-old design decision at a time.”

Leave a comment

Blog at WordPress.com.

Up ↑