Monday 9 am: Sunday’s revenue is ₹0
Monday, 9 am. TiffinGo’s city managers open the revenue dashboard: Sunday shows ₹0. Kabir’s nightly scripts ran on a timer, as always: extract at 1:00, transform at 1:30, publish at 2:00. But on Sunday the extract hit a timeout and failed. The transform ran anyway on empty input, and the publish step proudly wrote zeros.
Sir, every script “succeeded” except the first one. The timer started each one on time, but nobody told the transform that its input was missing.
That’s the difference between scheduling and orchestration, Kabir. A timer only knows the clock. An orchestrator knows that “transform” depends on “extract”, waits for it, retries it if it fails, and alerts a human instead of publishing nonsense.
Is that what Airflow does? It’s in almost every data engineering job post.
Yes. In Apache Airflow you describe the pipeline as a DAG, a graph of tasks with arrows for dependencies, and Airflow runs it on a schedule, once per day of data, keeping a history of every run.
And Sunday’s zeros? Can I fix them?
With a backfill: re-run the pipeline for Sunday after fixing things. If your tasks are idempotent, running them again gives the same correct result instead of duplicates. Today you’ll see retries rescue a 2 am failure, backfill a week safely, and learn why Airflow cares about a run’s logical date more than the clock.
What are data pipelines and orchestration?
A data pipeline is a sequence of automated steps that moves data from sources to destinations, transforming and validating it on the way (for example extract → validate → transform → load). Orchestration is the automated coordination of those steps: scheduling runs, enforcing dependencies, passing parameters such as the logical date, handling retries and timeouts, alerting on failure and recording run history. In Apache Airflow, a pipeline is defined in Python as a DAG (directed acyclic graph) of tasks.
A pipeline is a chain of jobs that turns raw data into something useful, like yesterday’s revenue table. An orchestrator such as Airflow runs the chain in the right order, waits when a step isn’t ready, retries steps that fail, and alerts people when something is really wrong.
Words you’ll meet in this lesson
0 1 * * * = 1:00 every day.An Airflow DAG (example)
This is what the daily orders pipeline looks like in Airflow 3 using the TaskFlow style. It is shown for reading; it needs an Airflow installation to run.
from datetime import datetime, timedelta
from airflow.sdk import dag, task # Airflow 2.x: from airflow.decorators import dag, task
@dag(schedule="0 1 * * *", # every day at 1:00 am
start_date=datetime(2026, 10, 1),
catchup=False,
default_args={"retries": 3, "retry_delay": timedelta(minutes=5)})
def daily_orders():
@task
def extract(ds=None): # ds = the logical date, e.g. "2026-10-19"
return f"s3://lake/bronze/orders/date={ds}/"
@task
def validate(path):
... # row counts, nulls, negative amounts
return path
@task
def transform(path):
... # submit a Spark job: bronze -> silver -> gold
transform(validate(extract()))
daily_orders()Pipelines and orchestration at three levels
Beginner A recipe, a cook and a kitchen manager
- A pipeline is a recipe: steps in order. Buy vegetables → wash → chop → cook → serve.
- Dependencies: you can’t chop before the vegetables arrive. In data, you can’t transform before you extract.
- A timer (cron) is an alarm that says “start chopping at 7:30”, even if the vegetables never came.
- An orchestrator (Airflow) is a kitchen manager: it starts each step only when the previous one is done, tries again if the shop was closed, and tells the owner if dinner will be late, instead of serving an empty plate.
Intermediate Airflow building blocks
- DAG: the pipeline as a directed acyclic graph of tasks, written in Python, with a schedule.
- Task / operator: one step (run SQL, call an API, submit a Spark job, run Python). Sensors wait for something, such as a file landing in S3.
- Scheduler and executor: the scheduler decides which tasks are ready; the executor runs them on workers.
- Logical date (data interval): each run processes one period of data, for example 19 October, even if it actually runs at 1 am on 20 October or a week later during a backfill.
- Retries, timeouts and alerts are settings, not code you write each time.
- Airflow orchestrates; it doesn’t crunch: heavy work runs in Spark, a warehouse or dbt; Airflow tells them when to run and checks the result.
- Similar tools: Dagster, Prefect, Azure Data Factory, AWS Step Functions, and managed Airflow (Amazon MWAA, Google Cloud Composer, Astronomer).
Advanced Idempotency, backfills and reliability
- Idempotent tasks: running a task twice for the same logical date gives the same result. Write by overwriting the date partition (or MERGE on keys), never by blind appends.
- Backfill: re-run past logical dates after a bug fix or a new column. Only possible cleanly if tasks are idempotent and parameterised by date, not by “now”.
- Incremental loads: process only new or changed data (by date partition or a watermark column such as
updated_at) instead of everything every night. - Late data: events can arrive days late. Common fix: each run also reprocesses the last N days, which is safe because tasks are idempotent.
- Quality gates: row-count, null and range checks between tasks stop bad data from being published (Great Expectations, dbt tests, Soda).
- Operations: SLAs and alerts for late pipelines, lineage to see what depends on what, and keeping DAG files light so the scheduler stays fast.
Exam tip: Define a data pipeline and orchestration, draw a small DAG (extract → validate → transform → load), and explain scheduling, dependencies, retries, backfill and idempotency. Name Apache Airflow with its parts: DAG, operator, sensor, scheduler, executor.
Kinds of pipelines, and cron vs an orchestrator
Batch ETL / ELT pipelines
Run on a schedule (hourly, nightly) over a period of data: extract from sources, load to the lake or warehouse, transform with Spark, SQL or dbt.
ScheduledStreaming pipelines
Run continuously: Kafka → stream processor → sinks. Still need orchestration for deployments, backfills and checks.
Always onEvent-triggered pipelines
Start when something happens: a file lands, an upstream DAG finishes, an API is called.
On demandML pipelines
Prepare features, train, evaluate and deploy models on a schedule or when data drifts.
Train + serveCron vs an orchestrator
| Cron / timer scripts | Orchestrator (Airflow, Dagster…) | |
|---|---|---|
| Dependencies | None: each script starts by the clock | Task waits for upstream tasks to succeed |
| Failures | Silent unless you build alerts | Retries, timeouts, alerts, marked failed in the UI |
| Backfills | Manual, error-prone | Built in: run past logical dates |
| History | Log files, if any | Every run, task, attempt and duration recorded |
| Parameters | Usually “today” | Logical date passed to every task |
| Best for | One small independent job | Multi-step pipelines across systems |
Write every task as a function of the logical date: “process orders for {ds} and overwrite date={ds}”. That one habit makes retries, re-runs and backfills safe, and it is the most common thing interviewers check in pipeline design questions.
Building a reliable daily pipeline
Draw the DAG
Each task lists what it depends on; the orchestrator works out a valid order (a topological sort) and runs independent tasks in parallel when it can.dag = {"extract": [], "validate": ["extract"], "transform": ["validate"], "publish": ["transform"], "email_report": ["publish"]} done, order = set(), [] while len(order) < len(dag): for task, deps in dag.items(): if task not in done and all(d in done for d in deps): order.append(task); done.add(task) print(" -> ".join(order))extract -> validate -> transform -> publish -> email_reportParameterise by logical date
The 19 October run reads and writes only 19 October’s partition, whether it runs at 1 am on the 20th or during a backfill next month.import datetime logical = datetime.date(2026, 10, 19) path = f"s3://lake/bronze/orders/date={logical}/" print("this run reads and writes only:", path)this run reads and writes only: s3://lake/bronze/orders/date=2026-10-19/Extract with retries
The source API sometimes times out at night. The extract task is set to
Most night-time failures are temporary: a busy API, a network blip, a restarting database. Retries turn them into short delays instead of morning emergencies.retries=3with a 5-minute delay (often growing each time: exponential backoff).Validate before publishing
A quality gate between transform and publish stops impossible data (a negative amount, zero rows, a sudden 90% drop) and alerts someone. Publishing nothing is better than publishing wrong numbers.rows = [120, 340, -90, 410] checks = {"has rows": len(rows) > 0, "no negative amounts": all(r >= 0 for r in rows)} print(checks, "-> PUBLISH" if all(checks.values()) else "-> STOP and alert"){'has rows': True, 'no negative amounts': False} -> STOP and alertPublish idempotently
Overwriting the date partition means a retry or a re-run produces the same result. With appends, every re-run would add the day’s revenue again.partitions = {} def publish(day, revenue): partitions[day] = revenue # overwrite the day’s partition publish("2026-10-19", 850); publish("2026-10-19", 850) # re-run print(partitions, "total:", sum(partitions.values())){'2026-10-19': 850} total: 850Monitor, alert, backfill
Airflow records every run. If Sunday failed, an alert goes out, the dashboard shows “data delayed”, and once fixed, the team clears and re-runs Sunday: a one-day backfill.
Good pipelines are judged on the bad days: how fast failures are noticed, how clearly they are reported and how safely they are repaired.
One night: what the managers see, and what Airflow does
Step through the six frames. Left: the dashboard the managers will open. Right: the DAG run behind it, including a failure and a retry.
- Sunday night: the dashboard shows the week, with Sunday “updating overnight”.
- Nothing visible changes for the managers while the pipeline works.
- Still waiting, as expected at this hour.
- Still updating.
- By morning Sunday’s revenue appears: ₹38.4 lakh.
- At 9 am the managers see fresh numbers, with a note that the quality checks passed.
- At 1:00 the scheduler creates the run for logical date 19 October (Sunday). Extract starts; the other tasks wait for it.
- Extract fails with an API timeout. Unlike Kabir’s timer scripts, the orchestrator does not start validate or transform on missing data.
- After a 5-minute retry delay, extract runs again and succeeds.
- Validate checks row counts, nulls and amounts; transform submits the Spark job.
- Publish overwrites the 19 October partition, so any re-run gives the same result.
- The run is marked successful, with every attempt, duration and log stored for later. (Times are illustrative.)
Real-time scenarios: pipelines on bad days
Three live scenarios: a failure that retries can’t fix, a week-long backfill after a bug, and an advanced look at logical dates and late-arriving data.
Real-time scenario 1A failure that retries can’t fix
Retries fix most night-time hiccups. But on Diwali night the source database is overloaded for hours. Watch how a well-built pipeline fails safely and recovers.
- The extract task fails and retries with growing delays (5, 10, 20 minutes), giving the busy source time to recover.
- After the last retry it is marked failed, and every downstream task is marked “upstream failed” instead of running on missing data.
- An alert with a link to the logs pages the on-call engineer.
- The dashboard shows “Sunday data delayed” instead of a wrong ₹0, so managers don’t make decisions on bad numbers.
- When the source is healthy, the engineer clears the failed task. It and all downstream tasks re-run for Sunday’s logical date, and the numbers are published.
Backfilling a week after a bug fix
A bug under-counted wallet payments for seven days. The code is fixed; now the past must be repaired. Watch why idempotent tasks make a backfill safe, and appends make it a disaster.
- For seven days, a conversion bug published revenue that was too low. The fix is now in the code.
- A backfill re-runs the fixed DAG for each logical date from 13 to 19 October, each run touching only its own day.
- If the publish step appended rows, the corrected rows would be added on top of the wrong ones, roughly doubling every day.
- Because publish overwrites each date partition, each day is replaced by its correct value, and running the backfill twice changes nothing.
- Tables built from these days (weekly and monthly summaries) are re-run too, in dependency order. (Figures are illustrative.)
Logical dates, late data and sensors
Real data doesn’t arrive on time. Orders from phones that were offline can show up days later. Watch how logical dates, a reprocessing window and a sensor keep daily numbers correct.
- Each run is responsible for one data interval. The run with logical date Sunday covers all of Sunday, midnight to midnight.
- It actually starts on Monday at 1 am, once Sunday is complete. Every task uses the logical date, so the same run gives the same answer if repeated later.
- On Tuesday, phones that were offline upload 1,200 orders that happened on Sunday. Sunday’s published revenue is now too low.
- A common fix: every daily run also re-processes the last three days. Because tasks overwrite their date partitions, re-processing is safe and late data is absorbed.
- A sensor can also make a run wait until the source signals that a day is complete (for example a
_SUCCESSmarker file) before extracting.
Where orchestration runs every day
1. Nightly business reporting
Most companies run hundreds of nightly DAGs: copy yesterday’s data from app databases, build silver and gold tables in Spark or dbt, check quality and refresh dashboards before offices open.
2. Machine-learning retraining
Recommendation and fraud models are retrained on a schedule: build features, train, evaluate against the current model, and deploy only if better, all as one orchestrated pipeline.
3. Regulatory and finance reports
Banks, NBFCs and e-commerce firms produce daily and monthly regulatory and tax reports with strict deadlines. Orchestrators track SLAs and alert long before a deadline is missed.
4. Keeping streaming systems healthy
Even real-time platforms need scheduled jobs: compacting small files, reprocessing late data, rebuilding aggregates, and running data-quality checks on stream outputs.
A pipeline as a DAG, run in order
The pipeline is a dictionary of tasks and the tasks each depends on. The program finds a valid order (a topological sort), runs each task for one logical date, retries a flaky step, and stops downstream tasks if something fails for good. Then try the challenges: parallel branches, quality gates, idempotent publishing, backfills and incremental loads.
dag = {"extract": [], "validate": ["extract"], "transform": ["validate"], "publish": ["transform"]}
attempts_needed = {"extract": 2} # extract fails once, then works
RETRIES = 3
def order(dag):
done, out = set(), []
while len(out) < len(dag):
for t, deps in dag.items():
if t not in done and all(d in done for d in deps):
out.append(t); done.add(t)
return out
logical_date = "2026-10-19"
for task in order(dag):
for attempt in range(1, RETRIES + 1):
if attempt >= attempts_needed.get(task, 1):
print(f"{logical_date} {task:9} success (attempt {attempt})")
break
print(f"{logical_date} {task:9} failed, retrying")
else:
print(f"{task} failed for good: downstream skipped, alert sent")
breakdag = {"extract": [], "validate": ["extract"], "transform": ["validate"], "publish": ["transform"]}Each task lists its upstream tasks. That is all an orchestrator needs to know the order.if t not in done and all(d in done for d in deps):A task is ready only when every task it depends on has finished.for attempt in range(1, RETRIES + 1):Retries handle temporary failures such as timeouts without waking anyone up.else:Thefor … elseruns only if no attempt succeeded: stop the pipeline instead of running on missing data.
Run it on your PC: a tiny orchestrator with retries and backfills
This program defines a four-task DAG, runs it in dependency order for a logical date, retries a flaky extract step, and publishes one JSON file per day into a warehouse folder. Then it backfills three days twice to prove that re-running is safe. Only the standard library is used. Save it as mini_orchestrator.py and run it.
DAG order, retries, idempotent backfill
"""A tiny orchestrator: tasks with dependencies, run in order, with retries,
one output partition per logical date, and a backfill that re-runs safely."""
import json, os, random
random.seed(1)
os.makedirs("warehouse", exist_ok=True)
ORDERS = { # pretend source system: orders per day
"2026-10-17": [120, 340, 90], "2026-10-18": [200, 150], "2026-10-19": [410, 80, 60, 300],
}
DAG = {"extract": [], "validate": ["extract"], "transform": ["validate"], "publish": ["transform"]}
def topo_order(dag):
done, order = set(), []
while len(order) < len(dag):
for task, deps in dag.items():
if task not in done and all(d in done for d in deps):
order.append(task)
done.add(task)
return order
def run_task(task, day, state, flaky):
if task == "extract":
if flaky and random.random() < 0.6: # the source API sometimes times out
raise TimeoutError("source API timed out")
state["rows"] = ORDERS[day]
elif task == "validate":
assert all(a > 0 for a in state["rows"]), "negative amount"
elif task == "transform":
state["summary"] = {"day": day, "orders": len(state["rows"]), "revenue": sum(state["rows"])}
elif task == "publish":
# idempotent: overwrite this day's partition instead of appending
with open(f"warehouse/daily_{day}.json", "w") as f:
json.dump(state["summary"], f)
def run_dag(day, flaky=False, retries=3, verbose=True):
state = {}
for task in topo_order(DAG):
for attempt in range(1, retries + 1):
try:
run_task(task, day, state, flaky)
if verbose:
print(f" {day} {task:9} ok" + (f" (attempt {attempt})" if attempt > 1 else ""))
break
except Exception as e:
print(f" {day} {task:9} failed: {e}" + ("; retrying" if attempt < retries else "; giving up"))
else:
print(f" {day} downstream tasks skipped, alert sent")
return
if not verbose:
print(f" {day} all tasks ok -> {state['summary']}")
print("Task order:", topo_order(DAG))
print("\nNightly run for 2026-10-19 (flaky source):")
run_dag("2026-10-19", flaky=True)
print("\nBackfill 2026-10-17 to 2026-10-19 (run twice to prove it is safe):")
for run in (1, 2):
print(f" run {run}:")
for day in ORDERS:
run_dag(day, verbose=False)
files = sorted(os.listdir("warehouse"))
total = sum(json.load(open("warehouse/" + f))["revenue"] for f in files)
print(f"\nPartitions in warehouse: {files}")
print(f"Total revenue across partitions: Rs {total} (not doubled, because publish overwrites)")
# In a terminal: python mini_orchestrator.pyRecorded from a real run with Python 3.13; the random seed makes it repeatable. Open the warehouse folder after running: there is exactly one file per day, even though the backfill ran twice. To try real Airflow, install it with Docker using the official quick-start, or use a managed service, and put the DAG shown in the Definition section into its dags folder.
Finding the run order, one line at a time
Three tasks are listed out of order. Watch the loop pick a task only when everything it depends on is already done.
Worked examples: real problems, solved and traced in memory
Four pipeline problems, from ordering tasks to sizing a backfill. Each runs live and is traced.
Read a cron schedule
Problem statement
Airflow schedules are often cron expressions with five fields: minute, hour, day of month, month, day of week. Explain three schedules in words.
0 1 * * * · 30 */4 * * * · 0 9 * * 1
0 1 * * * -> minute 0, hour 1, every day 30 */4 * * * -> minute 30, every 4 hours, every day 0 9 * * 1 -> minute 0, hour 9, on Mondays
Approach
- Split each expression into its five fields.
- Translate the minute and hour fields;
*means every,*/4means every 4. - Day of week 1 means Monday.
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.
Which tasks can run in parallel?
Problem statement
A DAG has a common extract, two independent cleaning branches and a final report that needs both. Group tasks into waves that can run at the same time, and find the shortest total time if each task takes the given minutes.
extract 10, clean_orders 20, clean_riders 5, report 8 (minutes)
wave: ['extract'] wave: ['clean_orders', 'clean_riders'] wave: ['report'] shortest total: 38 minutes (vs 43 one by one)
Approach
- A task can start when all its upstream tasks are done.
- Each wave takes as long as its slowest task.
- Total time = sum of wave times.
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.
How long can retries delay a pipeline?
Problem statement
A task retries up to 4 times with exponential backoff starting at 2 minutes (2, 4, 8, 16). Each attempt takes 3 minutes. What is the worst-case time before it finally fails, and does it still meet a 2-hour window?
4 retries, backoff 2 min doubling, 3 min per attempt
delays between attempts: [2, 4, 8, 16] worst case before failing: 45 minutes fits a 2-hour window
Approach
- There is one first attempt plus 4 retries: 5 attempts.
- Add the waiting time between attempts: 2 + 4 + 8 + 16.
- Compare the total with the time window.
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.
Plan a safe backfill
Problem statement
A bug affected 45 days. Each daily run takes 12 minutes, the cluster can run 6 days in parallel without hurting nightly jobs, and runs must finish before the 1 am nightly start, with 6 hours available. Plan the backfill.
45 days, 12 min per run, 6 parallel, 6 hours
batches: 8 | minutes needed: 96 nights required: 1
Approach
- Batches = days ÷ parallel runs, rounded up.
- Time = batches × minutes per run.
- If it doesn't fit, split across nights.
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 4.5 in one screen
- Pipeline = automated steps from source to destination. Orchestration = scheduling + dependencies + retries + alerts + history.
- Airflow: DAG of tasks; operators (do work), sensors (wait), scheduler (decides), executor (runs).
- Cron
0 1 * * *= 1:00 daily. Each run has a logical date / data interval. - Idempotent tasks: overwrite date partitions or MERGE; never blind appends.
- Backfill past dates safely only if tasks are idempotent and use the logical date, not now().
- Quality gates before publish; alerts and “data delayed” instead of wrong numbers.
- Incremental loads (watermarks), reprocessing windows for late data; Airflow orchestrates, Spark/SQL/dbt do the heavy work.
Interview and viva questions
What is a data pipeline?
A data pipeline is an automated sequence of steps that moves data from one or more sources to destinations, typically extracting, validating, transforming and loading it, so that it is available in a usable form (for example, a daily revenue table for dashboards). Pipelines can be batch (scheduled) or streaming (continuous).
What is Apache Airflow and what are its main components?
Apache Airflow is an open-source platform for authoring, scheduling and monitoring workflows as DAGs written in Python. Its main components are the scheduler (creates runs and decides which tasks are ready), the executor and workers (run tasks), the metadata database (stores run and task state), the web server/UI (monitoring and manual actions), and DAG files containing tasks built from operators and sensors.
What does idempotent mean in data pipelines and why is it important?
An idempotent task produces the same result no matter how many times it runs for the same input or logical date. It is important because tasks are retried after failures, re-run manually and backfilled; without idempotency these would duplicate or corrupt data. Common techniques are overwriting date partitions, MERGE/upsert on keys and deduplication.
What is the difference between scheduling and orchestration?
Scheduling only starts jobs at given times. Orchestration also manages dependencies between tasks, passes parameters such as logical dates, retries failures, enforces timeouts and SLAs, alerts on problems, records history and supports backfills across many systems.
How do you handle late-arriving data in batch pipelines?
Process data by event date into date partitions, and have each run reprocess a window of recent days (for example the last 3 days) using idempotent overwrites or MERGE, so late records are included. Use sensors or completeness markers to wait for sources, and monitor how late data typically arrives to choose the window.
Practice problems
Pipeline and orchestration exercises. The last two are business tasks.
Upstream tasks
Print the upstream tasks of “report”.
DAG of 4 tasks
['clean', 'enrich']
Show solution and explanation
dag = {"extract": [], "clean": ["extract"], "enrich": ["extract"], "report": ["clean", "enrich"]}
print(dag["report"])How it works: The report waits for both branches.
Partition path for a date
Build the output path for logical date 2026-10-19.
2026-10-19
s3://lake/gold/revenue/date=2026-10-19/
Show solution and explanation
ds = "2026-10-19"
print(f"s3://lake/gold/revenue/date={ds}/")How it works: Every run writes only its own date partition.
Runs per year
How many runs does a DAG scheduled every 4 hours create in a year?
every 4 hours
2190
Show solution and explanation
print(24 // 4 * 365)How it works: 6 runs a day for 365 days.
Failed tasks and their victims
If “clean” fails, print every task that can't run (all its downstream tasks).
DAG of 5 tasks
['enrich', 'report']
Show solution and explanation
dag = {"extract": [], "clean": ["extract"], "enrich": ["clean"], "report": ["enrich"], "audit": ["extract"]}
failed = {"clean"}
changed = True
while changed:
changed = False
for t, deps in dag.items():
if t not in failed and any(d in failed for d in deps):
failed.add(t); changed = True
print(sorted(failed - {"clean"}))How it works: Failure spreads down the DAG; “audit” still runs because it doesn't depend on “clean”.
Watermark load
Load rows with id greater than the last loaded id (42) and print the new watermark.
ids [40, 41, 43, 44, 47]
load: [43, 44, 47] | new watermark: 47
Show solution and explanation
ids = [40, 41, 43, 44, 47]
last = 42
new = [i for i in ids if i > last]
print("load:", new, "| new watermark:", max(new))How it works: Incremental loads read only new data, which keeps nightly runs short.
Detect a DAG cycle
A DAG must have no cycles. Print whether the graph has one.
a → b → c → a
cycle!
Show solution and explanation
graph = {"a": ["b"], "b": ["c"], "c": ["a"]}
def has_cycle(g):
visiting, done = set(), set()
def visit(n):
if n in visiting:
return True
if n in done:
return False
visiting.add(n)
found = any(visit(m) for m in g[n])
visiting.discard(n); done.add(n)
return found
return any(visit(n) for n in g)
print("cycle!" if has_cycle(graph) else "no cycle")How it works: Airflow refuses DAGs with cycles, because no valid order exists.
Reprocessing window
Each daily run reprocesses the last 3 days. For run date 2026-10-19, print the dates it overwrites.
2026-10-19, window 3
['2026-10-19', '2026-10-18', '2026-10-17']
Show solution and explanation
import datetime
run = datetime.date(2026, 10, 19)
print([str(run - datetime.timedelta(days=d)) for d in range(3)])How it works: Late data from the previous two days is picked up safely because each day is overwritten.
Meeting the 8 am SLA
The pipeline starts at 1:00. Tasks take 25, 40, 70 and 15 minutes in sequence. Worst case, extract also needs 2 retries of 25 minutes each with 10-minute waits. Does it finish by 8:00?
25, 40, 70, 15 min
normal: done at 3:30 OK worst: done at 4:40 OK
Show solution and explanation
normal = 25 + 40 + 70 + 15
worst = normal + 2 * (25 + 10)
for name, m in [("normal", normal), ("worst", worst)]:
end = 60 + m # minutes after midnight
print(f"{name}: done at {end // 60}:{end % 60:02d}", "OK" if end <= 8 * 60 else "LATE")How it works: SLA planning uses the worst case, including retries.
Cost of a wrong dashboard
A wrong revenue number leads to 3 hours of investigation by 5 people at Rs 1,500 per hour each, about twice a month. A quality-gate project costs Rs 1.2 lakh once. How many months to pay back?
5 × 3 h × Rs 1,500 × 2 per month
cost per month: 45000 payback months: 2.7
Show solution and explanation
monthly_cost = 5 * 3 * 1500 * 2
print("cost per month:", monthly_cost)
print("payback months:", round(1_20_000 / monthly_cost, 1))How it works: Quality gates are cheap compared with the cost of decisions made on wrong data.
Real-world scenario questions (asked in company interviews)
Pipeline and orchestration questions from real teams and interviews. Decide your answer first, then open the model answer.
Design a daily sales pipeline
Orders live in a PostgreSQL database. Finance wants yesterday’s revenue by city on a dashboard by 8 am every day.
The interviewer is checking: can you design a simple, reliable, orchestrated pipeline?
Show a strong answer
- Airflow DAG scheduled at 1 am, parameterised by the logical date.
- Extract yesterday’s orders (by
updated_at) to the lake asbronze/orders/date=…. - Validate: row count vs the source, no negative amounts, no null cities.
- Transform to a gold table
revenue_by_city, overwriting that date’s partition. - Publish to the dashboard; on failure, retries then an alert and a “delayed” banner.
- Add an SLA alert if not done by 7 am.
Avoid: a cron job per script with no dependencies or checks.
Re-running a job doubled the numbers
A failed job was re-run manually and the monthly revenue report now shows twice the real amount for that day.
The interviewer is checking: do you understand idempotency and how to make pipelines safe to re-run?
Show a strong answer
The load step appends rows, so each run adds the day again. Make the task idempotent: write to a date partition and overwrite it (INSERT OVERWRITE or replace the folder), or use MERGE on a unique key. Deduplicate the affected day, then re-run once more to prove nothing changes. Add a check that compares row counts with the source.
Avoid: asking people to “be careful not to run it twice”.
Why does the 19 Oct run happen on 20 Oct?
A new teammate is confused: the DAG run labelled 2026-10-19 started at 1 am on 20 October, and their task that uses datetime.now() processed the wrong day.
The interviewer is checking: do you understand logical dates and data intervals?
Show a strong answer
In Airflow, a run’s logical date marks the start of the data interval it processes. A daily run for 19 October can only start after the interval ends, so it runs on 20 October. Tasks must use the run’s logical date (ds, data_interval_start) rather than now(), so that retries and backfills process the intended day.
Avoid: changing the schedule to run “earlier” or hard-coding yesterday’s date.
“Should Spark run inside Airflow?”
A teammate wants to load a 200 GB dataset into a Python task inside Airflow because “Airflow already runs Python”.
The interviewer is checking: do you know what an orchestrator is for?
Show a strong answer
No. Airflow should coordinate work, not do heavy processing. Its workers are sized for many small tasks, and big in-memory work there will crash workers and slow the scheduler. Use Airflow to submit a Spark job (on Databricks, EMR, Dataproc or Kubernetes), a warehouse SQL statement or a dbt run, wait for it, check the result and move on.
Avoid: scaling Airflow workers to hundreds of GB of RAM to process data in tasks.