A data engineer's day: pipelines, migrations and the lakehouse

Most Hadoop knowledge ends up used in one job: data engineering. This lesson follows a data engineer through a working day, from a failed overnight pipeline to a migration check, and runs the modern piece of the stack on our own cluster: an Apache Iceberg lakehouse table on HDFS that supports updates, deletes, snapshots and time travel.

Module 13 · Hadoop in industry today⏱ 32 minIntermediateReal Iceberg table on HDFSArchitecture diagram + 2 animationsRuns in your browser (Python)8 practice problems
1 The story

A price was wrong for a month

Kabir has joined a fintech in Bengaluru as a data engineer. On his first Monday the canteen app team calls: chai was billed at ₹15 all of September, but the real price was ₹18, and a coffee supplier's orders turned out to be test data. "Fix the table, but finance must still be able to see what the report said last week." On a folder of CSV files that would mean rewriting everything and losing the old version. Arjun Sir shows him a table format that keeps both.

lake.canteen.orders (Apache Iceberg on HDFS) snapshot 1 · append 500,000 orders revenue ₹12,852,002 snapshot 2 · overwrite DELETE coffee −40,364 orders snapshot 3 · overwrite UPDATE chai 15 → 18 459,636 · ₹12,493,729 time travel: SELECT … VERSION AS OF snapshot 1 → still 500,000 / ₹12,852,002 Real run on our YARN cluster: Spark 4.0.4 + Iceberg 1.10.2, data files in HDFS. myphatshala.in
Kabir

In the Hadoop course we only ever wrote whole files. How do I change a few rows without breaking the September report?

Arjun Sir

With a table format like Apache Iceberg. The data still lives in HDFS as Parquet files, but Iceberg keeps a list of which files make up the table at each moment: a snapshot. DELETE and UPDATE write new files and a new snapshot; the old snapshot stays readable. That is the lakehouse idea: warehouse features on cheap lake storage.

Riya

And that's Kabir's whole job? Fixing tables?

Arjun Sir

That's one hour of it. Today we follow a data engineer through a real day: night jobs, a failed pipeline, a schema change, a backfill, a migration task. And we run the Iceberg part on our own cluster.

2 Definition

Pipelines, migrations and the lakehouse

Formal definition (write this in exams)

A data pipeline is an automated, scheduled sequence of steps that ingests data from sources, validates and transforms it through stages (commonly raw/bronze, cleaned/silver and aggregated/gold) and publishes it for consumers, run by an orchestrator such as Apache Airflow that manages dependencies, retries, backfills and SLAs. A migration moves data, metadata and workloads from one platform to another (for example from an on-premises Hadoop cluster to cloud object storage or to a lakehouse), through inventory, copy, dual-running and reconciliation, cut-over and decommissioning. A lakehouse stores data in open file formats on low-cost storage (HDFS or object stores) and adds warehouse capabilities through an open table format (Apache Iceberg, Delta Lake, Apache Hudi): ACID transactions, row-level updates and deletes, snapshots and time travel, schema and partition evolution, all exposed through a catalog to several engines (Spark, Trino, Flink, Hive).

In simple words

A data engineer builds and runs the machines that move data every day, moves them to new platforms when the company changes, and today usually stores the results in tables that behave like a database but live as files in the lake.

Words you'll meet in this lesson

DAGAn Airflow workflow: tasks and the order they must run in.
BackfillRe-running a pipeline for past dates, e.g. all of September.
IdempotentRunning a step twice gives the same result as running it once.
Data contractAn agreed schema and quality rules between a producer and its consumers.
Table formatIceberg, Delta, Hudi: metadata that turns files into a transactional table.
SnapshotThe exact set of files that formed the table at one commit.
Time travelQuerying a table as it was at an older snapshot.
ReconciliationProving old and new systems give the same counts and totals.

Architecture at a glance

A modern lakehouse on Hadoop or cloud storage. The green boxes are what we ran in this lesson; the rest is the usual company setup around it.

Sources apps · UPI events OLTP databases partner files (SFTP) Ingest Kafka (stream) batch / CDC loads Iceberg tables on HDFS / object store bronzeraw, as-is silverclean, typed goldbusiness KPIs catalog: Hive Metastore / Iceberg REST / Glue Spark Trino Flink Consumers BI dashboards ML features regulatory reports Airflow: DAGs, retries, backfills, SLAs (schedules every arrow) Ranger (who may read) · Atlas (lineage, catalogue) YARN or Kubernetes runs the engines' containers (our lab: YARN on 3 workers) Green = ran in our lab (Spark 4.0.4 + Iceberg 1.10.2 on HDFS) · grey = typical company setup, explained only myphatshala.in

From our lab (Spark 4.0.4 + Iceberg 1.10.2 on Hadoop 3.5.0, YARN)

Load500,000 orders from student_orders.csv into lake.canteen.orders, partitioned by days(ts); revenue ₹12,852,002
FixDELETE … WHERE item = 'coffee' and UPDATE … SET amount = 18 WHERE item = 'chai' → 459,636 orders, ₹12,493,729
Snapshotsappend (added 500,000) → overwrite (added 459,636, deleted 500,000) → overwrite (added 459,636, deleted 459,636): copy-on-write rewrites whole files
Time travelVERSION AS OF the first snapshot still answers 500,000 / ₹12,852,002
In HDFSdata: 31 folders (one per day + root), 90 files, 7,617,041 bytes · metadata: 14 files (v1–v3.metadata.json, snap-*.avro manifest lists, manifests)
Incidentfirst run failed: "Self-suppression not permitted", then "could only be written to 0 of the 1 minReplication nodes": the disk was 97% full

The job at three levels

Pick your level. New to this? Start at Beginner and climb.

Beginner A day in the life

  • A data engineer is the plumber of a company's data: makes sure every morning's reports and models get fresh, correct data.
  • Mornings start with checking last night's jobs. Days fill with fixing breaks, building new pipelines and answering "why is this number different?".
  • The tools change (Hadoop, Spark, cloud, lakehouse), the job stays: move data reliably, prove it is right, keep costs sane.

Intermediate Pipelines that survive Monday

  • Layers: bronze keeps raw data exactly as received (so you can always rebuild), silver cleans and types it, gold aggregates for the business.
  • Idempotent steps: each run overwrites its own partition (e.g. dt=2026-09-14) instead of appending, so a retry or backfill never doubles numbers.
  • Orchestration: Airflow DAGs declare dependencies, retries and SLAs; airflow dags backfill re-runs a date range. Oozie, the old Hadoop scheduler, was retired to the Apache Attic in February 2025.
  • Quality gates: row counts, null checks and reconciliations against the source fail the DAG before bad data reaches gold (counters from Lesson 9.4 are the MapReduce version of this).

Advanced Migrations and the lakehouse

  • Migration path: inventory (tables, jobs, users, sizes) → copy (DistCp and snapshot-diff, Lesson 12.2) → dual-run old and new → reconcile counts and sums → cut over consumers → decommission.
  • Hive tables to Iceberg: migrate in place (Iceberg reads the existing Parquet/ORC files and only writes metadata) or rewrite with CREATE TABLE AS SELECT to fix partitioning and file sizes (Lesson 7.5).
  • Jobs: MapReduce, Pig and Hive jobs are usually rewritten as Spark SQL or dbt models; the logic (keys, joins, aggregations from Modules 8–9) carries over.
  • Table formats: Iceberg, Delta Lake and Hudi all give ACID commits, snapshots, time travel and schema evolution; Iceberg's hidden partitioning (days(ts)) means queries filter on ts and never name the partition column.

Exam tip: For "What is a lakehouse?", write: open files (Parquet) on cheap storage + an open table format (Iceberg/Delta/Hudi) that adds ACID transactions, row-level changes, snapshots/time travel and schema evolution, read by several engines through a catalog. Give one example: our DELETE + UPDATE made snapshot 3 while snapshot 1 stayed readable.

3 Types and ways to use it

Table formats, pipeline styles and migration routes

Apache IcebergDelta LakeApache Hudi
OriginNetflix, Apache projectDatabricks, Linux FoundationUber, Apache project
StrengthEngine-neutral; hidden partitioning; partition evolution; REST catalogDeep Spark/Databricks integrationUpserts and incremental pulls for streaming CDC
Row changesCopy-on-write or merge-on-readCopy-on-write; deletion vectorsCopy-on-write or merge-on-read
Shipped in EMR 7.141.10.13.3.21.0.2

Pipeline and migration choices

Batch (daily/hourly)

Airflow runs Spark jobs per partition; simplest and cheapest. Most finance and reporting pipelines.

most common

Streaming

Kafka → Flink or Spark Structured Streaming → Iceberg every minute; for fraud alerts and live dashboards.

fresh

Lift and shift

Copy HDFS to object storage, run the same Hive/Spark jobs on a cloud cluster. Fast, keeps old problems.

quick

Modernise

Rewrite into a lakehouse: Iceberg tables, Spark SQL/dbt, Airflow. Slower, but fixes small files and old engines.

long-term

Animated example: one day at a fintech

Kabir's Tuesday 6 steps
Animated example 09:3010:1512:0014:3016:3018:30 09:30 · stand-up: settlement_daily DAG is red task silver_txns failed 3 retries at 02:10 gold tables still show yesterday · banks need the file by 11:00 10:15 · cause: a partner added a column bank_ref now arrives as 9 fields, parser expects 8 no data contract was agreed with this partner 12:00 · fix + backfill ALTER TABLE silver.txns ADD COLUMN bank_ref re-run dt=2026-10-20 · it overwrites its partition, no doubles 14:30 · migration: dual-run check old Hive job vs new Spark + Iceberg job, same day rows and ₹ totals must match before cut-over 16:30 · cost review 12 lakh files under 1 MB in bronze schedule compaction + expire old snapshots 18:30 · hand-over runbook updated · contract drafted with partner new quality check: field count per file done today ✓ root cause ✓ settlement file 12:40 ✓ dual-run matched ✓ compaction ticket ✓ a check so it can't happen silently again myphatshala.in

Make every step idempotent before you need a backfill: write each run's output to its own partition with overwrite, never blind append. Then "re-run September" is a calm command instead of a cleanup project.

4 Step by step

A lakehouse table on our Hadoop cluster

Spark 4.0.4 (Lesson 13.2) with the Iceberg runtime jar from Maven Central, on our YARN, writing to HDFS. The catalog is the simplest kind, a folder in HDFS (type=hadoop).

  1. Create a partitioned Iceberg table and load it

    q("CREATE TABLE lake.canteen.orders (order_id BIGINT, student STRING, ts TIMESTAMP, item STRING, amount INT) "
      "USING iceberg PARTITIONED BY (days(ts))")
    src.writeTo("lake.canteen.orders").append()
    -- 1. loaded +------+--------+ |orders|revenue | +------+--------+ |500000|12852002| +------+--------+
    days(ts) is hidden partitioning: Iceberg derives the day from the timestamp. Queries filter on ts and Iceberg prunes days on its own.
  2. Change rows with SQL

    DELETE FROM lake.canteen.orders WHERE item = 'coffee';
    UPDATE lake.canteen.orders SET amount = 18 WHERE item = 'chai';
    -- 2. after DELETE coffee + UPDATE chai price +------+--------+ |orders|revenue | +------+--------+ |459636|12493729| +------+--------+
    12,852,002 − 40,364 × 20 (coffee) + 149,669 × 3 (chai +₹3) = 12,493,729. On a plain folder of CSV or Parquet files this needs a full rewrite job.
  3. Every change is a snapshot

    SELECT snapshot_id, operation, summary['added-records'], summary['deleted-records']
    FROM lake.canteen.orders.snapshots ORDER BY committed_at
    |6640816061555354185|append |500000|NULL | |4428609581335996210|overwrite|459636|500000 | |1149537224652247478|overwrite|459636|459636 |
    Copy-on-write: the DELETE rewrote every data file without the coffee rows (500,000 out, 459,636 in), and the UPDATE rewrote them again. Old files stay until the snapshots that use them are expired.
  4. Time travel

    SELECT count(*), sum(amount) FROM lake.canteen.orders VERSION AS OF 6640816061555354185
    +------+--------+ |orders|revenue | +------+--------+ |500000|12852002| +------+--------+
    Finance's request from the story: last week's report can be reproduced exactly, while today's table is corrected.
  5. Evolve the schema

    ALTER TABLE lake.canteen.orders ADD COLUMN payment STRING
    |order_id|item |amount|payment| |1 |dosa |45 |NULL | |2 |samosa|18 |NULL |
    A metadata-only change: no data file is rewritten. Iceberg tracks columns by ID, so rename and drop are safe too (compare Avro's rules in Lesson 10.1).
  6. Look inside HDFS

    hdfs dfs -count /lakehouse/canteen/orders/data /lakehouse/canteen/orders/metadata
    31 90 7617041 /lakehouse/canteen/orders/data 1 14 75103 /lakehouse/canteen/orders/metadata v1.metadata.json v2.metadata.json v3.metadata.json snap-….avro (×3)
    It is still just HDFS: day folders of Parquet files plus JSON and Avro metadata. A commit is one atomic swap of the current metadata.json; that is where the transaction comes from.

Our first run on YARN failed with "IllegalArgumentException: Self-suppression not permitted", which hides the real cause. The next attempt showed it: "could only be written to 0 of the 1 minReplication nodes". The laptop disk was 97% full and the DataNodes had 1.4–1.8 GB left. Freeing space fixed it. Monitor DataNode free space (Lesson 12.4) and read past the first exception (Lesson 12.6).

5 See it happen

SQL on the left, files in HDFS on the right

Six steps of our Iceberg run. Watch which files each statement adds and which snapshot the table points to.

An Iceberg table, commit by commit 6 steps
What you see spark (YARN) · lake catalog CREATE TABLE … PARTITIONED BY days(ts)writeTo(…).append()orders 500000 revenue 12852002 DELETE … WHERE item = 'coffee'(40,364 rows gone) UPDATE … SET amount = 18 WHERE chaiorders 459636 revenue 12493729 SELECT … FROM orders.snapshots…5354185 append 500000…5996210 overwrite 459636 / 500000…2247478 overwrite 459636 / 459636 … VERSION AS OF …5354185orders 500000 revenue 12852002 ALTER TABLE … ADD COLUMN payment1 dosa 45 NULL2 samosa 18 NULL myphatshala.in
Behind the screen hdfs:/lakehouse/canteen/orders data/ (day folders) metadata/ set A · 30 days v1.metadata.json set B · no coffee v2.metadata.json set C · chai 18 v3.metadata.json copy-on-write: each change writes a new set current → snapshot 1 current → snapshot 2 current → snapshot 3 snap-*.avro lists name the files of each set read snapshot 1set A is still in HDFS, so it is readable v4 (schema +payment)only metadata changes, no data file rewritten a commit = atomically switching to a new metadata.json final: 90 data files, 7.6 MB · 14 metadata files myphatshala.in
6 Real-world use

Data engineering at work

settlementT+1 file

1. Payments: daily settlement

A payments company builds the next-day settlement files for partner banks from the previous day's transactions, with reconciliation against bank statements before anything is sent.

Transactions (Kafka)→Airflow + Spark + checks→Bank files by 11:00
Hive → Icebergin place

2. Telecom: migrating years of call records

An operator moves petabytes of Hive tables on HDFS to Iceberg: old partitions are migrated in place (metadata only), new data is written by Spark, and both run side by side for a month.

Hive tables→migrate + dual-run→Iceberg lakehouse

3. E-commerce: live sale dashboards

During a festival sale, clickstream flows through Kafka and Flink into Iceberg every minute, and Trino dashboards show orders per city while the batch layer keeps the full history.

Clicks→Kafka + Flink→Minute-fresh tables
as of31 March

4. Insurance: audit-ready reports

An insurer tags the snapshot used for each regulatory filing, so auditors can query exactly the data behind a report months later, even after corrections.

Corrected tables→snapshot tags + time travel→Reproducible filings
delete useron request

5. Ed-tech: deleting a learner's data

When a learner asks for their data to be erased, a row-level DELETE runs on every table that holds it, and old snapshots are expired so the deleted rows really leave storage.

Erasure request→DELETE + expire snapshots→Data gone everywhere

Simplified; organisations differ in tools and policies.

Sources (checked 22 Oct 2026): Apache Attic: retired projects (Oozie Feb 2025, Sqoop Jun 2021) · Amazon EMR 7.x application versions (Iceberg 1.10.1, Delta 3.3.2, Hudi 1.0.2) · Cloudera Base 7.3.2 summary (Iceberg REST catalog sharing) · endoflife.date: Apache Spark

7 Run the code

A table that keeps its history

A copy-on-write table in a few lines of Python: every change writes a new list of rows (new "files") and records a snapshot. The numbers are our lab's real totals, aggregated per item.

# per item: (orders, price) from our 500,000-order table
base = {"chai": (149669, 15), "coffee": (40364, 20), "dosa": (49927, 45), "idli": (44774, 30),
        "poha": (45336, 25), "samosa": (69884, 18), "thali": (30292, 80), "vada pav": (69754, 20)}
snapshots = []

def commit(op, rows):
    snapshots.append((len(snapshots) + 1, op, dict(rows)))     # copy-on-write: a whole new version

def stats(rows):
    return sum(n for n, p in rows.values()), sum(n * p for n, p in rows.values())

commit("append", base)
v2 = {k: v for k, v in snapshots[-1][2].items() if k != "coffee"}          # DELETE coffee
commit("overwrite", v2)
v3 = {k: (n, 18 if k == "chai" else p) for k, (n, p) in snapshots[-1][2].items()}   # UPDATE chai
commit("overwrite", v3)

for sid, op, rows in snapshots:
    print(sid, op.ljust(9), "orders %d, revenue %d" % stats(rows))
print("time travel to 1:", "orders %d, revenue %d" % stats(snapshots[0][2]))
1 append orders 500000, revenue 12852002 2 overwrite orders 459636, revenue 12044722 3 overwrite orders 459636, revenue 12493729 time travel to 1: orders 500000, revenue 12852002
  • snapshots.append((len(snapshots) + 1, op, dict(rows))) # copy-on-write: a whole new versionLike Iceberg's copy-on-write: a change writes new data and a new snapshot; the old one is untouched.
  • v3 = {k: (n, 18 if k == "chai" else p) for k, (n, p) in snapshots[-1][2].items()} # UPDATE chaiOur real UPDATE: chai from 15 to 18 rupees.
  • print("time travel to 1:", "orders %d, revenue %d" % stats(snapshots[0][2]))VERSION AS OF the first snapshot gave 500,000 and 12,852,002 in the lab.

Run it on your PC: an Iceberg lakehouse on the Module 8 lab

Use the Module 8 lab with Spark 4.0.4 installed as in Lesson 13.2. Download the Iceberg runtime for Spark 4.0 (iceberg-spark-runtime-4.0_2.13-1.10.2.jar, 47 MB) from Maven Central (repo1.maven.org/maven2/org/apache/iceberg/) and copy it into the namenode container. The table lives in hdfs://namenode/lakehouse. Keep at least a few GB free on your disk: our first attempt failed because the disk was 97% full.

Load, change, time travel, evolve

# lakehouse_iceberg.py: an Apache Iceberg table on HDFS (Spark 4.0 + Iceberg 1.10).
# Things a plain folder of Parquet files cannot do: row-level DELETE/UPDATE, snapshots, time travel,
# schema evolution - all as ordinary SQL, with the data still sitting in HDFS.
from pyspark.sql import SparkSession
spark = (SparkSession.builder.appName("lakehouse demo")
         .config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
         .config("spark.sql.catalog.lake", "org.apache.iceberg.spark.SparkCatalog")
         .config("spark.sql.catalog.lake.type", "hadoop")                       # catalog = a folder in HDFS
         .config("spark.sql.catalog.lake.warehouse", "hdfs://namenode/lakehouse")
         .getOrCreate())
spark.sparkContext.setLogLevel("ERROR")
q = lambda sql: spark.sql(sql)
def show(title, sql):
    print(f"-- {title}"); q(sql).show(truncate=False)

q("DROP TABLE IF EXISTS lake.canteen.orders PURGE")
q("CREATE TABLE lake.canteen.orders (order_id BIGINT, student STRING, ts TIMESTAMP, item STRING, amount INT) "
  "USING iceberg PARTITIONED BY (days(ts))")
src = (spark.read.csv("hdfs://namenode/user/hadoop/data/student_orders.csv", inferSchema=True)
       .toDF("order_id", "student", "ts", "item", "amount"))
src.writeTo("lake.canteen.orders").append()
show("1. loaded", "SELECT count(*) AS orders, sum(amount) AS revenue FROM lake.canteen.orders")
q("DELETE FROM lake.canteen.orders WHERE item = 'coffee'")                 # e.g. a supplier's orders were fake
q("UPDATE lake.canteen.orders SET amount = 18 WHERE item = 'chai'")       # price correction
show("2. after DELETE coffee + UPDATE chai price", "SELECT count(*) AS orders, sum(amount) AS revenue FROM lake.canteen.orders")
show("3. every change is a snapshot", "SELECT snapshot_id, operation, summary['added-records'] AS added, summary['deleted-records'] AS deleted FROM lake.canteen.orders.snapshots ORDER BY committed_at")
first = q("SELECT snapshot_id FROM lake.canteen.orders.snapshots ORDER BY committed_at LIMIT 1").first()[0]
show("4. time travel: the table as it was at the first snapshot",
     f"SELECT count(*) AS orders, sum(amount) AS revenue FROM lake.canteen.orders VERSION AS OF {first}")
q("ALTER TABLE lake.canteen.orders ADD COLUMN payment STRING")
show("5. schema evolution: a new column, old rows read as NULL", "SELECT order_id, item, amount, payment FROM lake.canteen.orders ORDER BY order_id LIMIT 3")
show("6. partitions Iceberg manages for us (first 3 days)", "SELECT partition, record_count, file_count FROM lake.canteen.orders.partitions ORDER BY partition LIMIT 3")
spark.stop()

# Inside lab8-namenode-1 (SPARK_HOME = where you unpacked Spark 4.0.4):
#   export HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
#   $SPARK_HOME/bin/spark-submit --master yarn --num-executors 3 --executor-memory 768m \
#     --executor-cores 1 --driver-memory 768m \
#     --jars iceberg-spark-runtime-4.0_2.13-1.10.2.jar lakehouse_iceberg.py
#   hdfs dfs -count /lakehouse/canteen/orders/data /lakehouse/canteen/orders/metadata
-- 1. loaded +------+--------+ |orders|revenue | +------+--------+ |500000|12852002| +------+--------+ -- 2. after DELETE coffee + UPDATE chai price +------+--------+ |orders|revenue | +------+--------+ |459636|12493729| +------+--------+ -- 3. every change is a snapshot +-------------------+---------+------+-------+ |snapshot_id |operation|added |deleted| +-------------------+---------+------+-------+ |6640816061555354185|append |500000|NULL | |4428609581335996210|overwrite|459636|500000 | |1149537224652247478|overwrite|459636|459636 | +-------------------+---------+------+-------+ -- 4. time travel: the table as it was at the first snapshot +------+--------+ |orders|revenue | +------+--------+ |500000|12852002| +------+--------+ -- 5. schema evolution: a new column, old rows read as NULL +--------+------+------+-------+ |order_id|item |amount|payment| +--------+------+------+-------+ |1 |dosa |45 |NULL | |2 |samosa|18 |NULL | |3 |samosa|18 |NULL | +--------+------+------+-------+ -- 6. partitions Iceberg manages for us (first 3 days) +------------+------------+----------+ |partition |record_count|file_count| +------------+------------+----------+ |{2026-09-01}|15047 |1 | |{2026-09-02}|15239 |1 | |{2026-09-03}|15504 |1 | +------------+------------+----------+ -- what the table looks like in HDFS 31 90 7617041 /lakehouse/canteen/orders/data 1 14 75103 /lakehouse/canteen/orders/metadata snap-1149537224652247478-1-2d52b3dc-93a0-4fc9-a2a5-88b7f44fd387.avro snap-4428609581335996210-1-b47002ce-1898-4225-a776-7bd11e2c684d.avro snap-6640816061555354185-1-ee05e8ec-2b89-41f5-9778-4b8136949e71.avro v1.metadata.json v2.metadata.json v3.metadata.json

Real output from our cluster (YARN client lines removed). Snapshot IDs are random and differ on every run; the counts and totals are the same.

8 Line by line

A backfill that cannot double count

Three days are loaded, day 2 is reloaded after a fix. Watch the partition dictionary: the reload replaces day 2 instead of adding to it.

Worked examples: real problems, solved and traced in memory

Four everyday data-engineering calculations from different industries.

Example 1BeginnerPipelines · backfill

How long will the backfill take?

Problem statement

A logistics company finds a bug in its delivery-time pipeline for the last 45 days. One day takes 12 minutes on the cluster; the team can run 5 days in parallel without hurting other jobs. How long is the backfill, and when is it done if it starts at 19:00?

Input
45 days · 12 min/day · 5 parallel
Expected output
9 waves, 108 minutes, done at 20:48

Approach

  1. Waves = days ÷ parallel, rounded up.
  2. Time = waves × minutes per day.
  3. Add to the start time.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 2IntermediateMigration · plan

A migration timeline

Problem statement

A telecom operator moves 600 Hive tables and 900 jobs from on-prem Hadoop to an Iceberg lakehouse. A team migrates 40 jobs a week; every batch then dual-runs for 2 weeks before cut-over; the data copy (DistCp + snapshot diffs) starts at week 0 and takes 6 weeks. When can the old cluster be switched off?

Input
900 jobs · 40/week · 2-week dual-run · 6-week copy
Expected output
jobs rewritten by week 23, last cut-over week 25

Approach

  1. Weeks to rewrite all jobs = 900 ÷ 40.
  2. The last batch still needs its 2 weeks of dual-running.
  3. The copy must finish too; take the later date.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 3IntermediateMigration · reconcile

Do the two systems agree?

Problem statement

An insurer dual-runs claims aggregation. Old Hive output: 1,20,450 claims, ₹84.3 crore. New Spark output: 1,20,438 claims, ₹84.29 crore. Is the difference acceptable if the rule is "counts exact, money within 0.01%"?

Input
counts and totals from both systems
Expected output
counts match: False | missing: 12
money diff: 0.012%

Approach

  1. Compare counts exactly.
  2. Compare money as a percentage.
  3. A mismatch means find the 12 missing claims before cut-over.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 4AdvancedLakehouse · storage

What does keeping snapshots cost?

Problem statement

A retail table is 8 TB and copy-on-write updates rewrite about 5% of its files every day. Snapshots are kept for 30 days for time travel. How much extra storage do the old files use, and what if retention drops to 7 days? (Replication or EC overhead not included.)

Input
8 TB · 5% rewritten/day · 30 vs 7 days
Expected output
30 days: 12.0 TB of old files (150% of the table)
 7 days: 2.8 TB of old files (35% of the table)

Approach

  1. Old files per day = 5% of 8 TB.
  2. Kept for the retention period.
  3. Compare.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

9 Quiz and revision

Check yourself

Quick revision

Lesson 13.4 in one screen

  • A data engineer moves data reliably every day, proves it is correct, migrates platforms and controls cost.
  • Pipelines: bronze (raw) → silver (clean) → gold (business); Airflow DAGs with retries, SLAs, backfills; Oozie retired Feb 2025.
  • Idempotent steps overwrite their own partition, so retries and backfills never double count.
  • Migration: inventory → copy (DistCp, snapshot diff) → dual-run → reconcile → cut-over → decommission.
  • Lakehouse = open files on cheap storage + table format (Iceberg/Delta/Hudi): ACID, row-level changes, snapshots, time travel, schema evolution.
  • Lab: 500,000 → 459,636 orders, ₹12,852,002 → ₹12,493,729; three snapshots; time travel restored the first; 90 data files after copy-on-write.
  • Expire old snapshots and compact small files, or storage grows with every change.

Interview and viva questions

What does a data engineer do every day?

Checks overnight pipelines and fixes failures, builds and changes pipelines (ingest, clean, aggregate), handles schema changes from source teams, runs backfills, adds data quality checks, supports analysts with "why is this number different" questions, works on platform migrations and reviews storage and compute costs.

What is a lakehouse?

A data architecture that keeps data in open file formats such as Parquet on low-cost storage (HDFS or object storage) and adds database features through an open table format (Apache Iceberg, Delta Lake or Apache Hudi): ACID transactions, updates and deletes, snapshots and time travel, schema evolution, read by several engines through a shared catalog.

Iceberg vs Delta Lake vs Hudi: which should I learn?

All three solve the same problem. Iceberg is engine-neutral and widely supported by Spark, Trino, Flink, Hive and the cloud platforms; Delta is closest to Spark and Databricks; Hudi is strong for streaming upserts. Learn the shared ideas (snapshots, copy-on-write vs merge-on-read, compaction, schema evolution) and one format in depth; switching later is easy.

How do you migrate a Hadoop cluster to the cloud or a lakehouse?

Take an inventory of data, tables, jobs and users; copy data with DistCp and snapshot-diff syncs; convert or rewrite tables (Hive to Iceberg in place or by rewrite); rewrite jobs (MapReduce, Pig, Hive) in Spark SQL or similar; dual-run old and new and reconcile counts and totals; cut consumers over; keep the old system read-only for a while; then decommission.

What is an idempotent pipeline?

A pipeline in which running a step again for the same input gives the same result, for example by overwriting the output partition for that date instead of appending to it. It makes retries and backfills safe and is one of the most important habits in data engineering.

Practice problems

Data-engineering exercises using our lakehouse run and everyday scenarios.

P1Easy

Check the revenue arithmetic

Recompute the revenue after DELETE coffee and UPDATE chai from the loaded revenue.

Input
12,852,002 · 40,364 coffee at ₹20 · 149,669 chai +₹3
Expected output
12493729
Show solution and explanation
print(12852002 - 40364 * 20 + 149669 * 3)
12493729

How it works: 12,493,729, exactly the lab result.

P2Easy

Rows written by copy-on-write

How many rows did Iceberg write in total across the three commits?

Input
500,000 + 459,636 + 459,636
Expected output
1419272
Show solution and explanation
print(500000 + 459636 + 459636)
1419272

How it works: 1,419,272 rows written to keep a 459,636-row table with full history: the price of copy-on-write.

P3Easy

Days in the table

The data folder had 31 directories. How many day partitions is that?

Input
31 dirs incl. data/ itself
Expected output
30 days (September 2026)
Show solution and explanation
print(31 - 1, "days (September 2026)")
30 days (September 2026)

How it works: 30 day folders plus the data/ folder itself.

P4Medium

A simple quality gate

Fail the load if more than 1% of rows have a missing amount or a negative one.

Input
a list of amounts
Expected output
2 bad rows (20%) FAIL
Show solution and explanation
amounts = [15, 20, None, 45, -18, 30, 25, 18, 20, 80]
bad = sum(1 for a in amounts if a is None or a < 0)
share = bad / len(amounts)
print(f"{bad} bad rows ({share:.0%})", "FAIL" if share > 0.01 else "OK")
2 bad rows (20%) FAIL

How it works: 20% bad rows: the gate fails the task and gold is not updated.

P5Medium

Schema change detector

Compare yesterday's and today's column lists from a partner and report what changed.

Input
two schemas
Expected output
added: ['bank_ref'] removed: []
Show solution and explanation
old = ["txn_id", "payer", "payee", "amount", "ts"]
new = ["txn_id", "payer", "payee", "amount", "ts", "bank_ref"]
print("added:", [c for c in new if c not in old], "removed:", [c for c in old if c not in new])
added: ['bank_ref'] removed: []

How it works: A new column: add it to silver (metadata-only in Iceberg) and update the data contract.

P6Medium

Small files after streaming

A stream commits every minute into 12 partitions. How many files a day, and how many after an hourly compaction into one file per partition?

Input
1 commit/min · 12 files/commit
Expected output
17280 files/day -> 288 after hourly compaction
Show solution and explanation
per_day = 60 * 24 * 12
print(per_day, "files/day ->", 24 * 12, "after hourly compaction")
17280 files/day -> 288 after hourly compaction

How it works: 17,280 tiny files a day become 288 (Lesson 7.5).

P7Business problem

Snapshot retention vs cost

Old snapshot files use 12 TB at 30-day retention. Storage costs ₹1,800 per TB per month. What does cutting retention to 7 days save per year?

Input
12 TB · ₹1,800/TB/month
Expected output
Rs 198,720 per year
Show solution and explanation
saved_tb = 12 * (1 - 7 / 30)
print(f"Rs {saved_tb * 1800 * 12:,.0f} per year")
Rs 198,720 per year

How it works: About ₹1.98 lakh a year (illustrative price), at the cost of a shorter time-travel window for audits.

P8Advanced

Merge-on-read vs copy-on-write

A 1,000-file table gets 50 small updates a day, each touching 20 files. Copy-on-write rewrites the touched files; merge-on-read writes one small delete file per update and compacts nightly. Compare files written per day.

Input
50 updates × 20 files
Expected output
copy-on-write: 1000 files/day | merge-on-read: 70 files/day
Show solution and explanation
cow = 50 * 20
mor = 50 + 20                 # 50 small delete files + nightly compaction rewriting about 20 files
print("copy-on-write:", cow, "files/day | merge-on-read:", mor, "files/day")
copy-on-write: 1000 files/day | merge-on-read: 70 files/day

How it works: 1,000 vs 70 files written: merge-on-read suits frequent small updates; readers pay a little to merge the delete files until compaction.

Real-world scenario questions (asked in company interviews)

Data-engineering interviews mix pipeline design, debugging stories and lakehouse questions.

Pipelines · reliabilityFresher

"Your nightly job failed. Walk me through your first hour."

The settlement file is due at 11:00.

The interviewer is checking: calm, systematic debugging and communication.

Show a strong answer
  • Tell stakeholders it is late and when the next update comes; check which task failed and its logs (Lessons 11.5, 12.6).
  • Find the cause (here: an upstream schema change), fix the smallest thing, re-run only the failed partition: safe because the step is idempotent.
  • Afterwards: a data contract or schema check so it fails loudly and early next time.
Lakehouse · designExperienced

"Finance wants corrected numbers but also last month's report exactly as published."

Corrections arrive weeks after month-end.

The interviewer is checking: snapshots, tags and retention.

Show a strong answer
  • Use a table format: apply corrections with UPDATE/MERGE; tag the snapshot used for each published report.
  • Reports query the tag (time travel); dashboards query the current snapshot.
  • Set snapshot retention to keep tagged snapshots and expire the rest to control storage.
Migration · planningExperienced

"Plan the move of a 2 PB Hadoop cluster to a lakehouse"

Hundreds of Hive tables, MapReduce and Pig jobs, an Oozie scheduler.

The interviewer is checking: phasing and risk control.

Show a strong answer
  • Inventory and prioritise (usage, owners, dependencies); retire unused tables first.
  • Copy with DistCp and snapshot diffs; migrate Hive tables to Iceberg in place or rewrite badly partitioned ones; move Oozie flows to Airflow and jobs to Spark SQL.
  • Dual-run per domain with reconciliation reports, cut over consumer by consumer, keep the old cluster read-only, then decommission.
Concepts · formatsFresher

"Why not just use Parquet files in folders?"

A follow-up after you mention Iceberg.

The interviewer is checking: what the table format adds.

Show a strong answer
  • Plain folders have no transactions: readers can see half-written data, and there is no safe UPDATE or DELETE.
  • A table format adds atomic commits, snapshots and time travel, schema and partition evolution, and file-level statistics for pruning.
  • The data is still Parquet, so any engine with a connector can read it.