Show us something you built
Kabir’s full-time interview at TiffinGo ends with a surprise: “Before the offer, show us a small pipeline you built yourself, end to end, on your own laptop.” Riya, who has followed every lesson with him, wants to build one too for her portfolio. Arjun Sir gives them both the same brief.
Three days of order data. Land it raw, store it as Parquet partitioned by date, clean it with quality checks, mask phone numbers, build gold tables, print a report. And it must be safe to run twice.
On a laptop? I thought big data needs a cluster.
The design is what matters, Riya. Bronze, silver, gold, partitions, a quality gate, idempotent writes: these are exactly what you’d build on Spark and a cloud lake. You’ll just run it with Python and Parquet first, and swap the engine later.
What will the reviewers look at?
Whether it runs with one command, whether the numbers are right and explained, whether bad data is handled, whether a re-run creates duplicates, and whether your README explains the decisions. Today you’ll build it step by step, run it, break it on purpose, and see how each piece maps to production tools.
The capstone: a mini lakehouse pipeline
A capstone project is an end-to-end project that integrates the skills of a course into one working system. Here it is a batch data pipeline implementing the medallion architecture: raw ingestion, a bronze layer of source data in a columnar format partitioned by date, a silver layer of deduplicated, validated and privacy-protected records, and a gold layer of aggregated business tables, with a data quality gate, idempotent partition overwrites and reproducible outputs.
You build a small but complete data pipeline on your own computer: messy raw data goes in, clean trustworthy business numbers come out, and running it again never creates duplicates. It uses the same design real companies use, only smaller.
Words you’ll meet in this lesson
column=value (for example date=2026-10-19) that engines read as a column.The brief
| Input | 3 days of food-delivery orders as JSON lines, about 20,000 rows a day, with duplicates, missing cities and invalid quantities |
|---|---|
| Output | Gold tables: revenue and order count by city per day; revenue by dish; plus a printed report |
| Rules | Quality gate at 2%; no raw phone numbers after bronze; identical results on re-run; one command to run |
| Tools | Python 3 and pyarrow (pip install pyarrow); optional: DuckDB, PySpark, Airflow for extensions |
The capstone at three levels
Beginner Build it and run it
- Generate three days of raw orders with realistic problems (duplicates, missing cities, zero quantities).
- Store raw data as files, then as Parquet in folders named
date=YYYY-MM-DD(bronze). - Clean it (silver): remove duplicates, quarantine bad rows, replace phone numbers with keys.
- Summarise it (gold): revenue by city per day and revenue by dish, then print a report.
Intermediate Make it reliable
- Idempotency: each step overwrites its own date partition, so re-running gives identical results.
- Quality gate: the run stops if more than 2% of rows fail checks; quarantined rows are kept for review.
- Repeatability: fixed random seeds per day, so anyone can reproduce your numbers.
- Logging: each step logs row counts, so a reviewer can follow where rows went.
- README: problem, architecture diagram, how to run, results, decisions and limitations.
Advanced Take it towards production
- Swap pyarrow for PySpark or DuckDB; store tables in Delta Lake or Iceberg for ACID MERGE and time travel.
- Schedule each step as a task in an Airflow DAG with retries, backfills and alerts.
- Ingest from Kafka instead of files for a streaming bronze layer; keep the batch gold for finance.
- Move storage to object storage (S3, ADLS, GCS or local MinIO), add tests and CI, data contracts and cost notes.
Exam tip: Describe an end-to-end pipeline using the medallion architecture: the purpose of each layer, the data quality checks in silver, how partitioning and idempotent overwrites make re-runs safe, and how the design maps to Spark, Delta Lake and Airflow in production.
Layers, folders and grading
| Layer | Folder | What happens | Course lessons used |
|---|---|---|---|
| Raw | lake/raw/ | The app’s export lands unchanged as JSON lines | 2.1 data types · 2.2 file formats |
| Bronze | lake/bronze/orders/date=…/ | Same rows, stored as Parquet, one folder per date | 2.2 Parquet · 3.2 partitioning |
| Silver | lake/silver/orders/date=…/ | Deduplicated, validated, phones replaced by keys; bad rows to quarantine | 5.1 quality and privacy · 4.5 idempotency |
| Gold | lake/gold/ | Revenue by city per day; revenue by dish | 4.x lakehouse and SQL engines |
| Report | terminal output | The numbers a manager would read | 1.1 value from data |
How reviewers grade a capstone
Runs first time
One command, clear requirements, fixed seeds, no hard-coded personal paths.
20%Correct and explained
Row counts reconcile at every step; numbers are checked and described in the README.
30%Reliable
Quality gate, quarantine, idempotent re-runs, sensible errors.
30%Communicated
README with architecture, decisions, limitations and next steps; clean code.
20%Write the README while you build, not after. Each time you make a decision (“overwrite the partition instead of appending”), add one line explaining why. That list becomes your best interview material.
Building the pipeline
Extract: land the raw data
Keeping raw data unchanged means you can always rebuild every later layer if a bug is found.extract(day)writes one JSON-lines file per day intolake/raw/, with duplicates, missing cities and zero quantities, exactly like a real export.Bronze: Parquet partitioned by date
Columnar files compress well and read fast; date folders let later steps and queries touch only the days they need. The folder is deleted and rewritten each run, so it is idempotent.day = "2026-10-19" print(f"lake/bronze/orders/date={day}/part-0.parquet")lake/bronze/orders/date=2026-10-19/part-0.parquetSilver: deduplicate
Retries create duplicate orders. Keeping the first occurrence of each order_id removes them; in Spark or Delta you would use dropDuplicates or MERGE.ids = ["A", "B", "A", "C", "B"] seen, keep = set(), [] for i in ids: if i not in seen: seen.add(i); keep.append(i) print(keep, "removed", len(ids) - len(keep))['A', 'B', 'C'] removed 2Silver: quality gate and masking
Rows with qty ≤ 0 or no city go to quarantine. Above 2% the pipeline stops. Phone numbers become salted-hash customer keys, so analysts never see them.rows, bad = 21_429 - 150, 100 print(f"failure rate {bad / rows:.2%}", "-> continue" if bad / rows <= 0.02 else "-> STOP")failure rate 0.47% -> continueGold: business tables
Gold tables are small, fast and named in business language, the only layer most users ever query.gold()reads all silver partitions, groups by date and city and by dish, and writes two small Parquet tables for dashboards.Prove it: re-run and compare
The script runs the whole pipeline a second time and checks that the gold table is identical.
This one check demonstrates idempotency, the property interviewers ask about most for pipelines.
One run: the terminal, and the lake folder
Step through the six frames. Left: the terminal output. Right: files appearing in the lake.
- Kabir runs the script. Day 1’s raw export is extracted: 20,005 rows.
- Silver for day 1: 19,755 clean rows, 150 duplicates removed, 100 quarantined.
- All three days processed.
- Gold built from 59,682 clean orders.
- The report prints revenue by city.
- The pipeline runs again and confirms identical gold output.
- A JSON-lines file appears in lake/raw/. Raw data is kept exactly as received.
- The day is written to bronze as Parquet under date=2026-10-17, then cleaned into silver; failing rows go to a separate quarantine partition.
- Every layer now has one folder per date. A later query for one day reads only that folder.
- The gold step reads all silver partitions together and writes two small business tables.
- The report and any dashboard read only gold tables, which are tiny compared with raw data.
- Re-running deletes and rewrites each partition instead of appending, so the lake still has 14 files and the totals are unchanged: idempotency in practice.
Real-time scenarios: reconcile, break and scale
Three live scenarios: reconciling every row of one day (real numbers from the program below), breaking idempotency on purpose, and an advanced map from the capstone to production tools.
Real-time scenario 1Account for every row
These figures come from the program in “Run it on your PC” below. Reviewers trust pipelines whose row counts add up at every step. Watch one day reconcile.
- On 19 October the export has 21,429 rows, including duplicates from retries and some broken records.
- Deduplicating on order_id removes 150 rows.
- 100 rows fail the quality checks, 0.47% of the remaining rows, so the gate lets the run continue and the rows are kept in quarantine.
- 21,429 − 150 − 100 = 21,179 clean rows in silver: every row is accounted for.
- Over three days, 59,682 clean orders reach gold. A small table like this in the README shows a reviewer you understand your data.
Break it on purpose: append vs overwrite
The fastest way to understand idempotency is to break it. Change the write to append and watch retries and backfills multiply revenue.
- The first run writes 19 October’s silver and gold data: about ₹44 lakh of revenue (₹43.96 lakh, the sum of the four cities in the report).
- An orchestrator retries the task after a timeout. If the step appends, the same day’s rows are written again and revenue doubles.
- A backfill re-runs the day once more. The dashboard now shows three times the real figure, with no error anywhere.
- With partition overwrite (the capstone’s design: delete the date folder, then write), each run replaces the day completely.
- Retries and backfills become safe. In Delta Lake you would use
replaceWherefor a date orMERGEon order_id; in Iceberg, overwrite-partitions or MERGE.
From your laptop to a production lakehouse
Reviewers often ask: “How would this work at 1,000 times the data?” Watch each part of the capstone map to its production equivalent.
- Raw JSON files become a Kafka topic or an export landing in object storage; pyarrow becomes PySpark on a managed cluster.
- Plain Parquet folders become Delta Lake or Iceberg tables, adding ACID commits, MERGE for upserts, schema enforcement and time travel.
- The loop over dates becomes an Airflow DAG with tasks for bronze, silver and gold, retries, alerts and backfills by date.
- Print statements become structured logs, metrics and alerts, and quality checks move into a tool such as Great Expectations or dbt tests. The local folder becomes S3, ADLS or GCS with IAM.
- The architecture itself does not change, which is exactly why this capstone is a strong portfolio piece: it proves you understand the design, not just one tool.
Extend your capstone
1. Query the lake with DuckDB
Install DuckDB and run SQL directly on the Parquet folders: SELECT city, SUM(amount) FROM 'lake/silver/orders/*/*.parquet' GROUP BY city.
2. Rewrite silver in PySpark
Replace the pyarrow code with Spark DataFrames: dropDuplicates, filter, withColumn, partitionBy("date"). Compare speed and code.
3. Orchestrate with Airflow
Turn extract, bronze, silver and gold into four tasks in a daily DAG with retries and a backfill for October.
4. Add tests and CI
Write pytest tests for dedup, the quality gate and masking; run them on every push with GitHub Actions; add the badge to your README.
README template for your capstone
| Section | What to write |
|---|---|
| Problem | One paragraph: daily food-delivery orders need trustworthy revenue reporting. |
| Architecture | Diagram: raw → bronze → silver (+ quarantine) → gold → report, with tools. |
| How to run | pip install pyarrow then python capstone_pipeline.py; expected output. |
| Data and quality | Row reconciliation table per day; quality rules and the 2% gate. |
| Decisions | Why Parquet, why date partitions, why overwrite, why hashed keys. |
| Results | Revenue by city and dish; run time. |
| Limitations and next steps | Single machine, no ACID tables, no scheduler: how you would add Spark, Delta, Airflow. |
The whole pipeline in miniature
This browser version uses only plain Python: raw records go through bronze, silver and gold as lists and dictionaries, with deduplication, a quality gate and masking. Then try the challenges: idempotent partitions, a new quality rule, a new gold table, a broken gate and a reconciliation check.
import hashlib
raw = [ # order_id, date, city, qty, price, phone
("A1", "10-19", "Pune", 1, 140, "9876543210"), ("A2", "10-19", "Thane", 2, 120, "9123456780"),
("A1", "10-19", "Pune", 1, 140, "9876543210"), ("A3", "10-19", None, 1, 220, "9988776655"),
("A4", "10-19", "Pune", 0, 60, "9090909090"), ("A5", "10-19", "Thane", 1, 220, "9811122233"),
]
bronze = {"10-19": list(raw)} # one partition per date
seen, silver, quarantine = set(), [], []
for oid, day, city, qty, price, phone in bronze["10-19"]:
if oid in seen:
continue # duplicate from a retry
seen.add(oid)
if city is None or qty <= 0:
quarantine.append(oid)
continue
key = hashlib.sha256(("salt:" + phone).encode()).hexdigest()[:8]
silver.append((oid, day, city, qty * price, key))
gold = {}
for oid, day, city, amount, key in silver:
gold[city] = gold.get(city, 0) + amount
print("silver:", len(silver), "| quarantine:", quarantine)
print("gold revenue by city:", gold)if oid in seen:Deduplicate on the business key before anything else.key = hashlib.sha256(("salt:" + phone).encode()).hexdigest()[:8]Phones never reach silver; a salted hash keeps customers countable.
Run it on your PC: the full capstone pipeline
Install pyarrow once (pip install pyarrow), save this as capstone_pipeline.py in an empty folder and run it. It creates a lake/ folder with raw, bronze, silver (orders and quarantine) and gold layers for three days, prints a report, then re-runs everything and proves the results are identical. Open the folders to see the partitions.
Raw → bronze → silver → gold, then an idempotency test
"""Capstone: a mini lakehouse pipeline on your PC.
raw JSON lines -> bronze Parquet (partitioned by date) -> silver (clean, dedup,
quality gate, masked PII) -> gold (daily city revenue, top dishes) -> report.
Every step is idempotent: run the script twice and the results are identical.
Needs: pip install pyarrow"""
import hashlib, json, os, random, shutil, time
import pyarrow as pa
import pyarrow.compute as pc
import pyarrow.parquet as pq
LAKE = "lake"
DATES = ["2026-10-17", "2026-10-18", "2026-10-19"]
CITIES = ["Pune", "Mumbai", "Thane", "Nashik"]
DISHES = {"Veg thali": 140, "Paneer roll": 120, "Dal khichdi": 110, "Chicken biryani": 220, "Poha": 60}
def inr(n):
"""Indian digit grouping: 1117740 -> 11,17,740"""
s = str(int(n))
head, tail = s[:-3], s[-3:]
groups = []
while len(head) > 2:
groups.insert(0, head[-2:])
head = head[:-2]
return ",".join(([head] if head else []) + groups + [tail])
def log(step, msg):
print(f"[{step:7}] {msg}")
def extract(day):
"""Simulate the app's raw export: JSON lines with real-world problems."""
rnd = random.Random(day) # same day -> same data (repeatable)
path = f"{LAKE}/raw/orders_{day}.jsonl"
os.makedirs(os.path.dirname(path), exist_ok=True)
rows = []
for i in range(rnd.randint(18_000, 22_000)):
dish = rnd.choice(list(DISHES))
rows.append({"order_id": f"{day[-5:].replace('-', '')}-{i:06d}", "ts": f"{day}T{rnd.randint(7, 22):02d}:{rnd.randint(0, 59):02d}:00",
"city": rnd.choice(CITIES), "dish": dish, "qty": rnd.choice([1, 1, 1, 2, 3]),
"price": DISHES[dish], "phone": f"9{rnd.randint(100000000, 999999999)}"})
for r in rnd.sample(rows, 60):
r["qty"] = 0 # invalid quantity
for r in rnd.sample(rows, 40):
r["city"] = None # missing city
rows += rnd.sample(rows, 150) # retries create duplicates
with open(path, "w") as f:
for r in rows:
f.write(json.dumps(r) + "\n")
return path, len(rows)
def bronze(day, raw_path):
"""Raw copy as Parquet, one folder per date. Overwrite = idempotent."""
rows = [json.loads(line) for line in open(raw_path)]
table = pa.Table.from_pylist(rows)
out = f"{LAKE}/bronze/orders/date={day}"
shutil.rmtree(out, ignore_errors=True)
os.makedirs(out)
pq.write_table(table, f"{out}/part-0.parquet")
return table
def silver(day, table):
"""Deduplicate, check quality, mask PII. Stop if too many rows fail."""
seen, keep = set(), []
ids = table.column("order_id").to_pylist()
for i, oid in enumerate(ids):
if oid not in seen:
seen.add(oid)
keep.append(i)
dedup = table.take(keep)
dupes = table.num_rows - dedup.num_rows
good_mask = pc.and_(pc.greater(dedup["qty"], 0), pc.is_valid(dedup["city"]))
good, bad = dedup.filter(good_mask), dedup.filter(pc.invert(good_mask))
fail_rate = bad.num_rows / dedup.num_rows
if fail_rate > 0.02:
raise RuntimeError(f"quality gate failed for {day}: {fail_rate:.2%}")
keys = [hashlib.sha256(("salt:" + p).encode()).hexdigest()[:10] for p in good["phone"].to_pylist()]
good = good.drop(["phone"]).append_column("customer_key", pa.array(keys))
good = good.append_column("amount", pc.multiply(good["qty"], good["price"]))
for name, t in [("orders", good), ("quarantine", bad.drop(["phone"]))]:
out = f"{LAKE}/silver/{name}/date={day}"
shutil.rmtree(out, ignore_errors=True)
os.makedirs(out)
pq.write_table(t, f"{out}/part-0.parquet")
return good, dupes, bad.num_rows, fail_rate
def gold():
"""Business tables built from all silver partitions."""
orders = pq.read_table(f"{LAKE}/silver/orders", columns=["date", "city", "dish", "amount", "customer_key"])
orders = orders.set_column(0, "date", pc.cast(orders["date"], pa.string())) # partition column -> plain text
by_city = orders.group_by(["date", "city"]).aggregate([("amount", "sum"), ("amount", "count")])
by_city = by_city.sort_by([("date", "ascending"), ("amount_sum", "descending")])
by_dish = orders.group_by("dish").aggregate([("amount", "sum")]).sort_by([("amount_sum", "descending")])
os.makedirs(f"{LAKE}/gold", exist_ok=True)
pq.write_table(by_city, f"{LAKE}/gold/daily_city_revenue.parquet")
pq.write_table(by_dish, f"{LAKE}/gold/dish_revenue.parquet")
return by_city, by_dish, orders.num_rows
def run():
start = time.time()
for day in DATES:
raw_path, n_raw = extract(day)
log("extract", f"{day}: {n_raw:,} raw rows")
t = bronze(day, raw_path)
good, dupes, bad, rate = silver(day, t)
log("silver", f"{day}: {good.num_rows:,} clean, {dupes} duplicates removed, {bad} quarantined ({rate:.2%})")
by_city, by_dish, n = gold()
log("gold", f"{n:,} orders across {len(DATES)} days -> 2 gold tables")
return by_city, by_dish, round(time.time() - start, 1)
if __name__ == "__main__":
shutil.rmtree(LAKE, ignore_errors=True)
by_city, by_dish, secs = run()
print("\nRevenue by city, 2026-10-19 (Rs):")
for r in by_city.to_pylist():
if r["date"] == "2026-10-19":
print(f" {r['city']:7} {inr(r['amount_sum']):>11} ({inr(r['amount_count'])} orders)")
print("Top dishes, all 3 days (Rs):")
for r in by_dish.to_pylist()[:3]:
print(f" {r['dish']:16} {inr(r['amount_sum']):>11}")
first = pq.read_table(f"{LAKE}/gold/dish_revenue.parquet").to_pylist()
print("\nRe-running the whole pipeline to test idempotency...")
run()
second = pq.read_table(f"{LAKE}/gold/dish_revenue.parquet").to_pylist()
print("Gold identical after re-run:", first == second)
files = sum(len(fs) for _, _, fs in os.walk(LAKE))
print(f"Lake folder: {files} files under ./{LAKE}/")
# In a terminal: pip install pyarrow
# python capstone_pipeline.pyRecorded from a real run with Python 3.13 and pyarrow 26. Data is generated with a fixed seed per day, so your counts will match. It takes a few seconds on a normal laptop. Delete the lake/ folder any time; the script rebuilds it.
Silver, one record at a time
Four raw records pass through deduplication and the quality check. Watch the seen set, the clean list and the quarantine fill up.
Worked examples: real problems, solved and traced in memory
Four tasks you will meet while building and reviewing the capstone. Each runs live and is traced.
Build Hive-style partition paths
Problem statement
Generate the bronze folder path for each date in a list, in the date=YYYY-MM-DD format engines understand.
3 dates
lake/bronze/orders/date=2026-10-17/part-0.parquet lake/bronze/orders/date=2026-10-18/part-0.parquet lake/bronze/orders/date=2026-10-19/part-0.parquet
Approach
- Loop over dates.
- Format the path with an f-string.
- Print each path.
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.
Keep the latest version of each order
Problem statement
Sometimes duplicates are updates. Keep the record with the latest timestamp for each order_id.
(order_id, ts, status)
{'A1': ('10:20', 'delivered'), 'A2': ('10:05', 'placed')}Approach
- Track the best record per order_id in a dictionary.
- Replace it when a later timestamp arrives.
- Print the final records.
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.
Daily revenue and order count by city
Problem statement
From silver rows, build a gold table of (date, city) → revenue and number of orders, sorted by revenue.
(date, city, amount) rows
('10-19', 'Pune') revenue 360 orders 2
('10-19', 'Thane') revenue 300 orders 2Approach
- Use a dictionary keyed by (date, city).
- Add revenue and increment the count.
- Sort by revenue, highest first.
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.
Prove a step is idempotent
Problem statement
Write a test that runs a step twice on the same input and checks that the stored result is identical, for both an append step and an overwrite step.
a day of rows, two write functions
write_append idempotent: False write_overwrite idempotent: True
Approach
- Write two versions of the step: append and overwrite.
- Run each twice into a fresh store.
- Compare the store after one run and after two.
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.
Check yourself
Quick revision
Lesson 7.1 in one screen
- Raw (unchanged) → bronze (Parquet by date) → silver (dedup, quality gate, quarantine, masked PII) → gold (business tables).
- Hive-style partitions: lake/bronze/orders/date=YYYY-MM-DD/ enable pruning and per-day rewrites.
- Idempotency: overwrite the date partition (or MERGE); never blind append. Prove it by re-running.
- Reconcile: raw = clean + duplicates + quarantined, for every day.
- Quality gate at 2%: continue and alert below it, stop above it.
- Production map: Kafka/exports, Spark, Delta/Iceberg, Airflow, monitoring, object storage.
- README: problem, diagram, how to run, reconciliation, decisions, results, limitations.
Interview and viva questions
What is a good capstone project for big data or data engineering?
An end-to-end pipeline that ingests realistic messy data, stores it in layers (bronze, silver, gold) using a columnar format partitioned by date, applies deduplication and data quality checks with quarantine, protects personal data, produces business tables, and is idempotent and reproducible. It should run with one command and have a README with architecture, results and decisions.
Can I build a big data pipeline without a cluster?
Yes. The architecture matters more than the size: Python with pyarrow, DuckDB or Polars can process millions of rows on a laptop using the same layers, partitions and quality rules. You can later run the same design on Spark and cloud storage, and explain how it would scale in interviews.
What is the medallion architecture?
A layered lakehouse design: bronze holds raw data in a better storage format, silver holds cleaned, deduplicated and conformed data, and gold holds aggregated, business-ready tables. Each layer is rebuilt from the previous one, which makes debugging and reprocessing straightforward.
How do I make a data pipeline idempotent?
Design each step so that re-running it for the same input gives the same result: overwrite the output partition for the processed date, or use MERGE/upsert on a business key, deduplicate inputs, and avoid side effects such as blind appends or sending duplicate notifications. Test it by running the step twice and comparing outputs.
What should a data engineering project README include?
The problem, an architecture diagram, exact steps to run, the data sources and quality rules, a row reconciliation or validation summary, key design decisions and trade-offs, the results, and limitations with next steps. Add screenshots or sample output and a test or CI badge if available.
Practice problems
Small exercises that extend the capstone. The last two are business-style reviews.
Partition path
Print the silver path for date 2026-10-20.
2026-10-20
lake/silver/orders/date=2026-10-20/part-0.parquet
Show solution and explanation
print("lake/silver/orders/date=2026-10-20/part-0.parquet")How it works: Consistent paths make partitions easy to read and overwrite.
Count duplicates
Print how many duplicates are in ["A", "B", "A", "C", "A"].
IDs
2
Show solution and explanation
ids = ["A", "B", "A", "C", "A"]
print(len(ids) - len(set(ids)))How it works: Total minus unique = duplicates.
Order amount
qty 3, price 140. Print the amount.
3 × 140
420
Show solution and explanation
print(3 * 140)How it works: Derived columns such as amount are computed in silver.
Failure rate
18,898 rows after dedup, 100 bad. Print the failure rate to two decimals and the gate decision at 2%.
100 / 18,898
0.53% continue
Show solution and explanation
r = 100 / 18_898
print(f"{r:.2%}", "continue" if r <= 0.02 else "stop")How it works: The gate protects gold from bad batches.
Reconcile three days
Raw [20005, 18998, 21429], 150 dupes and 100 bad per day. Print total clean rows.
as given
59682
Show solution and explanation
print(sum(r - 150 - 100 for r in [20_005, 18_998, 21_429]))How it works: Matches the 59,682 orders in gold.
Indian number format
Print 1117740 with Indian digit grouping (11,17,740).
1117740
11,17,740
Show solution and explanation
s = str(1117740)
head, tail = s[:-3], s[-3:]
parts = []
while len(head) > 2:
parts.insert(0, head[-2:]); head = head[:-2]
print(",".join(([head] if head else []) + parts + [tail]))How it works: Lakh/crore grouping: last three digits, then pairs.
Latest status wins
From [("A1","10:00","placed"),("A1","10:30","delivered")], print the latest status.
2 versions
delivered
Show solution and explanation
rows = [("A1", "10:00", "placed"), ("A1", "10:30", "delivered")]
print(max(rows, key=lambda r: r[1])[2])How it works: Upserts keep the most recent version of each key.
Revenue share
City revenue: Mumbai 11,17,740, Thane 11,05,820, Nashik 10,89,740, Pune 10,82,230. Print Mumbai’s share.
4 cities
25.4%
Show solution and explanation
rev = {"Mumbai": 1117740, "Thane": 1105820, "Nashik": 1089740, "Pune": 1082230}
print(f"{rev['Mumbai'] / sum(rev.values()):.1%}")How it works: Gold tables answer business questions like this instantly.
Storage estimate
In the capstone run, one day (about 20,000 rows) is about 2.9 MB as raw JSON and 0.4 MB as bronze Parquet. Print a year of each in MB and the compression ratio.
2.9 MB vs 0.4 MB per day
raw 1058 MB, bronze 146 MB, 7.2x smaller
Show solution and explanation
raw, parquet = 2.9, 0.4
print(f"raw {raw * 365:.0f} MB, bronze {parquet * 365:.0f} MB, {raw / parquet:.1f}x smaller")How it works: Measured file sizes from your own run make the best README evidence for choosing Parquet.
Real-world scenario questions (asked in company interviews)
Questions reviewers and interviewers ask about capstone projects. Decide your answer first, then open the model answer.
“Walk me through your pipeline.”
A reviewer opens your capstone repository and asks for a two-minute walkthrough.
The interviewer is checking: can you explain design and data flow clearly?
Show a strong answer
Problem → layers → numbers → decisions: “Three days of orders land as JSON in raw. Bronze stores them as Parquet partitioned by date. Silver removes 150 duplicates a day by order_id, quarantines about 0.5% invalid rows behind a 2% gate and replaces phones with hashed keys. Gold has revenue by city and dish. Every step overwrites its date partition, so re-runs are identical; the script proves it by re-running. Next I’d move to Delta Lake and Airflow.”
Avoid: reading the code line by line.
“What happens if the silver step fails halfway?”
The reviewer asks about partial failures.
The interviewer is checking: do you understand atomicity and recovery?
Show a strong answer
In the capstone, a failed write may leave a partly written folder, but the next run deletes and rewrites that date, so it self-heals. In production, write to a temporary location and move atomically, or use Delta Lake or Iceberg, where a commit is atomic: readers see the old version until the new one is complete. Orchestrator retries are safe because the step is idempotent.
Avoid: saying “it won’t fail”.
“What if each day had 50 crore rows?”
The reviewer asks how the design changes at 25,000 times the volume.
The interviewer is checking: can you separate design from engine?
Show a strong answer
The layers, partitioning, quality gate and idempotent writes stay. Changes: run on Spark with many partitions instead of one Python process; dedup with dropDuplicates or MERGE; use Delta or Iceberg tables; partition by date (and maybe city) with file compaction; read silver incrementally for gold; store in object storage; add monitoring and cost controls.
Avoid: proposing a different architecture for every tool.
“Why hash phone numbers instead of deleting them?”
The reviewer asks about the customer_key column.
The interviewer is checking: can you explain pseudonymisation and its limits?
Show a strong answer
Analysts need to count distinct and repeat customers without seeing phone numbers. A salted hash gives the same key for the same phone, so joins and counts work. It is pseudonymisation, not anonymisation: with the salt someone could re-identify customers, so the salt must be secret (a secrets manager, not the code) and access to bronze, which still has phones, must be restricted.
Avoid: claiming hashing makes the data anonymous.