DR. ATABAK KH
Cloud Platform Modernization Architect specializing in transforming legacy systems into reliable, observable, and cost-efficient Cloud platforms.
Certified: Google Professional Cloud Architect, AWS Solutions Architect, MapR Cluster Administrator
Migration plans talk about tools and timelines. What broke after go-live, in my experience, was usually schema drift, partitioning assumptions, batch dependencies, cost models, and expectations nobody wrote down.
Context: Early 2024, many enterprises were still moving away from legacy data warehouses and distributed big-data estates because of licensing costs, scarce skills, and growing analytics demand. The technical cutover was rarely the hardest part. The difficult part was changing behavior accumulated over years: physical layouts, implicit dependencies, security conventions, and reporting expectations.
These are generalized lessons from common migration patterns and personal technical study. The examples are illustrative; they do not describe an employer, client, or specific production system.
In a tightly coupled legacy estate, a column rename might break one report and get fixed locally. In a cloud platform, the same change can affect pipelines, semantic models, APIs, and ML features at once.
What helped: checks at ingest, quarantine for bad records, versioned curated tables. Not “we’ll notice”.
Filesystem date folders, database indexes, and engine-specific pruning strategies do not automatically become efficient cloud-warehouse layouts.
What helped: redesign partitions around how people actually filter, usually time plus one or two selective dimensions. Benchmark with representative data and queries. Do not assume the old execution plan predicts the new platform’s behavior.
-- Illustrative warehouse layout; syntax varies by platform
CREATE TABLE curated.fact_events (
event_id STRING,
user_id STRING,
event_type STRING,
event_ts TIMESTAMP,
payload JSON
)
PARTITION BY DATE(event_ts)
CLUSTER BY user_id, event_type;
-- always include a filter on the partition column
SELECT COUNT(*)
FROM curated.fact_events
WHERE DATE(event_ts) BETWEEN '2024-01-01' AND '2024-01-07'
AND user_id = 'u-123';
“The sales mart runs after finance close” was often a human rule, not an explicit dependency. In a new orchestrator, missing that relationship can produce silently stale or incomplete data, especially across time zones and fiscal calendars.
What helped: model calendars and upstream SLAs as real dependencies. Write down latest acceptable start time, not only job duration.
Fixed-capacity platforms often hid the marginal cost of inefficient scans. Usage-based cloud services make that cost visible. A query that was merely slow before can now be both slow and expensive.
What helped: serving layers, clustering, partition filters, materialize heavy aggregates, query budgets - before cutover, not after the first invoice.
Same report, same number, same time. Migration changes freshness, rounding, nulls, timezones. Without a parity agreement, trust dies even when the new platform is better.
What helped: signed reconciliation on critical metrics during parallel run, with variance thresholds agreed upfront.
| What we brought over | What it cost us |
|---|---|
Huge wide tables, SELECT * |
Scan cost, slow BI |
| Giant star schemas rebuilt every night | Long fragile critical path |
| Heavy UDFs in SQL | Slot time, hard to test |
| Tiny files every micro-batch, no compaction | Metadata pain |
| Shared service account | Audit and blast radius |
Moving is not enough. You usually have to change the shape of the work.
A minimal, platform-neutral reconciliation query for a parallel run might look like this:
WITH source_data AS (
SELECT order_date, COUNT(*) AS rows_old, SUM(revenue) AS rev_old
FROM legacy.orders_daily GROUP BY 1
),
target_data AS (
SELECT order_date, COUNT(*) AS rows_new, SUM(revenue) AS rev_new
FROM curated.orders GROUP BY 1
)
SELECT
COALESCE(o.order_date, n.order_date) AS order_date,
rows_old, rows_new,
CASE
WHEN o.order_date IS NULL OR n.order_date IS NULL OR rows_old = 0 THEN NULL
ELSE 1.0 * (rows_new - rows_old) / NULLIF(rows_old, 0)
END AS row_delta_pct,
CASE
WHEN o.order_date IS NULL OR n.order_date IS NULL OR rev_old = 0 THEN NULL
ELSE 1.0 * (rev_new - rev_old) / NULLIF(rev_old, 0)
END AS rev_delta_pct
FROM source_data o
FULL OUTER JOIN target_data n ON o.order_date = n.order_date
WHERE o.order_date IS NULL
OR n.order_date IS NULL
OR rev_old = 0
OR ABS(1.0 * (rev_new - rev_old) / NULLIF(rev_old, 0)) > 0.005
ORDER BY order_date;
When we ran old and new together, the things that mattered were:
Database specialists and data engineers can disagree about who owns performance and cost. BI teams may rebuild shadow marts on top of the new warehouse. Delivery partners leave, and nobody knows why the dependency graph is ordered that way.
Naming owners before cutover helped. Training people to interpret query plans, resource contention, and pruning helped more than teaching only how to rerun a job.
Legacy data-platform migrations fail quietly when teams copy old shapes instead of redesigning for access patterns, dependencies, cost, and trust. The breakage is predictable. Plan for that - not only for volume and bandwidth.
This is a personal blog. The views, thoughts, and opinions expressed here are my own and do not represent, reflect, or constitute the views, policies, or positions of any employer, university, client, or organization I am associated with or have been associated with.