State the data grain, assumptions, metric, leakage or failure risk, and how you would validate the result before discussing tools.
Question set
11 detailed answers
01What is the difference between ETL and ELT, why did ELT win with cloud warehouses, and when does ETL still make sense?
middle
Short answer: ETL transforms data before loading (on a separate engine), while ELT first loads raw data into the warehouse and runs transformations inside it in SQL. ELT became the default because cloud MPP warehouses (Snowflake, BigQuery, Redshift) scale compute cheaply and separate storage from compute.
In depth:
- ETL — transform on an intermediate engine (historically pricey ETL servers). Only clean, ready data lands in the warehouse. Downside: logic is locked in the tool and the raw data is lost.
- ELT — load the raw layer as-is, transform via dbt/SQL. Upside: raw is preserved, transformations are version-controlled in Git, and the warehouse handles scaling.
- Why ELT won — separating storage/compute made in-warehouse compute cheap and elastic; analysts prefer keeping logic in SQL.
| Criterion | ETL | ELT |
|---|---|---|
| Where transform runs | On a separate engine | Inside the warehouse |
| Raw layer | Usually lost | Preserved |
| Scaling | Bound by the engine | Elastic (MPP) |
| Typical stack | Informatica, SSIS | Fivetran + dbt + Snowflake |
When ETL still fits: heavy preprocessing before the warehouse, masking PII/compliance (raw cannot be loaded), or a source easier to coerce to schema on the fly.
⚠️ Common mistake: thinking ELT means "no transformations." The transformations didn't disappear — they just moved into the warehouse and run after loading.
02Batch vs streaming ingestion: what are the tradeoffs in latency, cost, and complexity, and how do you choose?
middle
Short answer: Batch processes data in chunks on a schedule (latency of minutes to hours, simple and cheap), while streaming processes events as they arrive (seconds of latency, but more expensive and harder to operate). The freshness requirement drives the choice, not fashion.
In depth:
- Batch — read the accumulated volume per interval, run it through Airflow/dbt/Spark. Easier to debug, easy to backfill, cheaper. Latency = window size.
- Streaming — Kafka + Flink/Spark Structured Streaming process events continuously. Needed for fraud, alerts, real-time dashboards. They require managing state, watermarks for late events, exactly-once.
- Micro-batch — a compromise: process in tiny windows (seconds), simpler than streaming with nearly the same freshness.
| Criterion | Batch | Streaming |
|---|---|---|
| Latency | Minutes–hours | Seconds |
| Cost | Lower | Higher (24/7) |
| Complexity | Low | High (state, late events) |
| Backfill | Trivial | Hard |
| When | Reporting, ML features | Fraud, alerts, real-time |
How to choose: start from "how fresh must the data be for the decision?" If "an hour is fine" — batch. If seconds matter and money is on the line — streaming.
⚠️ Common mistake: building streaming because "it's modern" when the business is fine with hourly freshness — you pay in complexity and cost with no added value.
03What is idempotency in pipelines, why is it critical, and how do you make a load idempotent?
senior
Short answer: An idempotent load produces the same result when re-run on the same data — a second run creates no duplicates. This is critical because tasks fail and retry, and without idempotency a retry doubles the data.
In depth:
- Overwrite the partition — instead of a blanket
INSERT, overwrite a day's partition:INSERT OVERWRITE PARTITION. A rerun simply replaces the old data with the same chunk. - MERGE / upsert by key — match on the business key and update rather than inserting blindly. A rerun updates the same rows.
- Deduplicate by key — if the source emits duplicates, keep the latest version via
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC).
-- Idempotent: overwrite the partition for a specific date
INSERT OVERWRITE TABLE sales PARTITION (dt = '2026-06-30')
SELECT * FROM staging_sales WHERE dt = '2026-06-30';
-- Idempotent: upsert by business key
MERGE INTO dim_customer t
USING staging_customer s
ON t.customer_id = s.customer_id
WHEN MATCHED THEN UPDATE SET t.name = s.name, t.updated_at = s.updated_at
WHEN NOT MATCHED THEN INSERT (customer_id, name, updated_at)
VALUES (s.customer_id, s.name, s.updated_at);
⚠️ Common mistake: a blind INSERT INTO ... SELECT with no key or partition overwrite. When a failed task retries, rows are inserted again — you get silent duplicates that surface in reports as doubled revenue.
04What incremental loading strategies exist: full vs incremental, watermark, and CDC?
senior
Short answer: A full load re-reads the entire source each time — simple but doesn't scale. Incremental pulls only what's new/changed since the last run using a watermark (high-water mark), while CDC captures the changes themselves (insert/update/delete) from the database log.
In depth:
- Full load —
TRUNCATE+ reload. Fine for small tables, honest about deletes, but expensive and slow on large ones. - Incremental by watermark — store the previous run's max
updated_at/idand take rows strictly greater than it. Cheap, but blind to physicalDELETEs. - CDC — read the redo/WAL log (Debezium, Fivetran) and apply the change stream, including deletes. Closer to real-time, but harder to operate.
Watermark (high-water mark):
last run: max(updated_at) = 2026-06-29 23:00
│
▼
SELECT * FROM src
WHERE updated_at > '2026-06-29 23:00' ← delta only
│
▼
MERGE into target table → new watermark = 2026-06-30 23:00
⚠️ Common mistake: incrementing on updated_at when the source doesn't stamp it on every change (or using a non-strict >=, catching boundary duplicates). Physical deletes are also lost this way — they need CDC or a periodic full reconcile.
05How do you safely backfill a pipeline: reprocessing history, partition-based backfills, and avoiding double-counting?
senior
Short answer: A backfill reprocesses historical periods when logic changes or gaps need filling. Do it safely only through idempotent per-partition overwrites: each day is recomputed independently and overwrites its own partition instead of appending rows.
In depth:
- Partition by date — the task should take the date as a parameter and touch only that day's partition (
INSERT OVERWRITE PARTITION (dt=...)). Then a backfill = running the same task for past dates. - Idempotency is mandatory — overwrite, not
INSERT. Otherwise re-running an already-computed day doubles the data. - Control the load — cap parallelism (Airflow
max_active_runs) so a 2-year backfill doesn't crush the cluster and the source.
Partition-based backfill (reprocess a date range):
for dt in 2026-01-01 .. 2026-06-30:
OVERWRITE partition(dt) ← each day is independent and idempotent
┌────────┬────────┬────────┬────────┐
│ dt=1 │ dt=2 │ dt=3 │ ... │ overwrite, not append
└────────┴────────┴────────┴────────┘
⚠️ Common mistake: double-counting on backfill — running a recompute on top of already-loaded data with INSERT instead of OVERWRITE, or aggregates that sum old and new rows. Always overwrite the whole partition.
06How does orchestration work in Airflow (DAGs, tasks, operators, scheduling), and how does it differ from Dagster/Prefect?
middle
Short answer: Airflow describes a pipeline as a DAG — a graph of tasks with dependencies; each task is created by an operator and runs on a schedule. The key principle is that tasks must be idempotent and retryable, because Airflow re-runs them on failure.
In depth:
- DAG — a directed acyclic graph: nodes are tasks, edges are dependencies (
a >> b). The scheduler runs a task once its upstream dependencies are done. - Operators — task templates:
PythonOperator,BashOperator, sensors, service-specific ones. Each run is tied to alogical_date(the interval), which is what makes backfill possible. - Idempotency and retries — you set
retries; a task must re-run safely, so we write via partition overwrite.
from airflow import DAG
from airflow.operators.python import PythonOperator
import pendulum
with DAG(
dag_id="daily_sales",
schedule="@daily",
start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
catchup=True, # enables backfill of past intervals
default_args={"retries": 2},
) as dag:
extract = PythonOperator(task_id="extract", python_callable=do_extract)
load = PythonOperator(task_id="load", python_callable=do_load)
extract >> load # dependency: load after extract
Airflow vs Dagster/Prefect: Airflow is the mature standard, task- and schedule-oriented. Dagster thinks in data assets with typing/tests; Prefect emphasizes dynamic flows and an ergonomic Python API. New projects often pick Dagster for data assets, but Airflow remains the default by ecosystem.
⚠️ Common mistake: a task that can't be re-run (e.g., it appends to a file or sends an email on every run). On the first retry you get duplicates or spam.
07What does dbt provide as the transform layer in ELT: models, ref, tests, materializations — and why do analysts and engineers like it?
middle
Short answer: dbt is the T in ELT: you write transformations as SQL SELECT models, and dbt builds a dependency graph from them via ref(), materializes the result in the warehouse, and runs tests. People love it because analytics becomes version-controlled, testable, documented code.
In depth:
- Models and ref — each model is a
SELECT; references via{{ ref('stg_orders') }}give dbt the DAG and run order. No manualCREATE TABLE. - Materializations —
view(light, always fresh),table(fast reads),incremental(computes only what's new by watermark),ephemeral(CTE). The choice balances recompute cost against read speed. - Tests and docs —
not_null,unique,relationshipsright in YAML; plus auto-generated docs and lineage.
# models/marts/orders.yml
models:
- name: orders
config:
materialized: incremental
unique_key: order_id # upsert by key on incremental runs
columns:
- name: order_id
tests: [unique, not_null]
- name: customer_id
tests:
- relationships:
to: ref('customers')
field: customer_id
| Materialization | When |
|---|---|
| view | Light transforms, need freshness |
| table | Heavy reads, frequent queries |
| incremental | Large fact tables, delta only |
⚠️ Common mistake: using table where a view suffices, and conversely running incremental without a correct unique_key — then the incremental run duplicates rows on reprocessing.
08How do you design a robust pipeline DAG: dependencies, retries, alerting, atomicity, and why you shouldn't build one giant task?
senior
Short answer: A good DAG is split into small, atomic, idempotent tasks with explicit dependencies, each with retries and alerts. A monolithic task is an anti-pattern: when it fails at 90%, you re-run everything from scratch and can't see where it broke.
In depth:
- Split into atomic tasks — extract, load, transform, quality-check separately. A transform failure doesn't force re-pulling extract; retries are targeted.
- Explicit dependencies — the graph's edges reflect the real data order; no hidden coupling via "it'll run by the clock."
- Atomic writes — a task either fully applies its result (partition overwrite) or nothing. No half-written states.
- Retries + alerting — exponential backoff for transient failures; alert to Slack/PagerDuty on a final fail and on SLA miss.
Bad (monolith) Good (atomic tasks)
┌──────────────┐ extract ─► load ─► transform ─► test ─► publish
│ do_everything│ │ │ │ │
│ (fails at │ retry retry retry alert on fail
│ 90% → all │
│ over again)│ a transform failure ⇒ only transform retries
└──────────────┘
⚠️ Common mistake: one 500-line "do-everything" task with no intermediate states. Any failure = a full restart, you can't tell where it broke, and you can't reuse the steps.
09Scheduling and triggering pipelines: cron/interval vs event/sensor-driven, and how do you handle late-arriving data?
middle
Short answer: A pipeline is triggered either on a schedule (cron/interval — simple and predictable) or by an event (a sensor waits for a file/message/upstream readiness — more precise but more complex). Late-arriving data is handled by re-running its period's partition or by windows with a watermark.
In depth:
- Cron / interval — "every day at 03:00." Pro: predictable and simple. Con: it may start before the data is actually ready.
- Event / sensor-driven — the task waits for a trigger: a file lands in S3, a message hits the queue, an upstream DAG finishes. More precise on readiness, but you must manage timeouts and "hangs forever."
- Late-arriving data — yesterday's data shows up today. Options: an idempotent re-run of yesterday's partition, a rolling recompute window (last N days), or a watermark in streaming that tolerates lateness.
| Trigger type | Pros | Cons |
|---|---|---|
| Cron/interval | Simple, predictable | Data may not be ready |
| Sensor/event | Fires on actual readiness | Timeouts, harder to debug |
⚠️ Common mistake: a rigid cron with no upstream-readiness check — the task starts on time, reads half the data, and silently emits an incomplete result. Or a sensor with no timeout that hangs and holds a scheduler slot.
10Delivery guarantees in a pipeline: at-least-once vs exactly-once, and how do you achieve effectively-once with idempotent writes?
senior
Short answer: At-least-once guarantees an event isn't lost but allows duplicates on retries; exactly-once means exactly one processing, but it's expensive and not always achievable in a distributed system. In practice you aim for effectively-once: at-least-once delivery plus an idempotent write that yields the effect of a single processing.
In depth:
- At-most-once — at most one attempt; on failure the event is lost. Fine only if loss isn't critical.
- At-least-once — retry until success, no loss, but duplicates are possible. The default of most queues (Kafka, SQS).
- Exactly-once — exactly one, end-to-end. Expensive: needs transactions, idempotent producers, coordination. Rarely needed in pure form.
- Effectively-once — accept duplicate delivery but kill it at the write: upsert/MERGE by key, dedup by event_id, partition overwrite. The result is indistinguishable from exactly-once.
| Guarantee | Loss | Duplicates | Cost |
|---|---|---|---|
| At-most-once | Yes | No | Low |
| At-least-once | No | Yes | Medium |
| Exactly-once | No | No | High |
| Effectively-once | No | Killed at write | Medium |
⚠️ Common mistake: promising "exactly-once" while actually running at-least-once with a non-idempotent write — duplicates from the queue happily reach the mart and inflate your metrics.
11How does a layered pipeline architecture work (raw/staging/curated, or bronze/silver/gold), and why keep an immutable raw layer?
middle
Short answer: Data flows through layers: raw/bronze — untouched source data, staging/silver — cleaned and typed, curated/gold — business marts for consumers. You keep an immutable raw layer so you can replay any transformation from scratch without hitting the source again.
In depth:
- Bronze (raw) — data as-is, append-only, unmodified. This is your source of truth and safety net.
- Silver (staging) — cleaning, type coercion, deduplication, key standardization. Quality is fixed here.
- Gold (curated) — aggregates and marts for specific metrics/dashboards. What the business sees.
Source
│
▼
┌─────────┐ cleaning, ┌─────────┐ aggregation, ┌────────┐
│ BRONZE │─ typing ─────►│ SILVER │─ business ───►│ GOLD │──► BI / ML
│ (raw, │ dedup │(staging,│ logic │(metric │
│ immutab)│ │ clean) │ │ marts) │
└─────────┘ └─────────┘ └────────┘
you can replay everything from bronze without touching the source
Why immutable raw: the source may change or become unavailable; when a new logic bug appears, you recompute silver/gold from bronze. Plus audit and reproducibility.
⚠️ Common mistake: loading straight into curated with no raw layer. Find a transformation bug — there's nothing to replay, the historical source data is gone, and you're stuck living with corrupted marts.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.