Data Pipeline Architecture and Design Questions
End-to-end design of data pipelines: source-to-sink flow, staging layers, idempotency, backfills, and reprocessing. Covers choosing between batch and streaming stages, decoupling ingestion from transformation, and designing for evolvability. The foundational systems-design skill for a data engineering interview.
You're loading a large fact table daily and need to choose a partition scheme. Would you partition by date, by a key like user ID, or some hybrid, and what breaks if you pick wrong?
Sample Answer
Direct answer
For a table loaded daily where most queries filter by time, partition by date; only add a secondary key (a hybrid scheme) once a large share of the expensive queries also filter or join on that key at high selectivity. Partitioning by a high-cardinality key like a user identifier alone, instead of date, breaks the thing a daily load needs most: a clean, cheap way to append "yesterday's data" as one new partition.
Structured elaboration
| Scheme | Good for | What breaks if you pick it wrong |
|---|---|---|
| By date only | time-range scans, straightforward daily append (a new partition per load), retention by dropping old partitions | Point or join-heavy user-level queries scan entire day partitions and filter in-engine, wasting I/O |
| By a high-cardinality key only | user-centric joins and lookups | Daily loads have nothing natural to append to, since new rows scatter across all existing partitions instead of forming one new unit; retention by date becomes a delete-and-rewrite operation instead of a partition drop; and if the key is skewed, a handful of partitions absorb most of the traffic |
| Hybrid (date partition, plus clustering or bucketing on the key within each partition) | both time-range and user-centric access, without giving up simple daily append | Adds a compaction and maintenance cost to keep the intra-partition layout useful; a poorly chosen secondary key still fails to deliver the user-query benefit |
The decision criteria are what the daily load pattern actually needs (a clean new unit to append) and what the majority of expensive downstream queries filter or join on. If the workload is overwhelmingly time-range, do not add the secondary dimension speculatively. If user-level joins are frequent and expensive, add clustering or bucketing on that key inside the date partition rather than replacing the date partition outright.
Worked example
A fact table ingests 20 million rows/day, retained for 2 years:
730×20,000,000=14,600,000,000 rows total
Partitioned by date, a 30-day trend query scans only the partitions in that range:
30×20,000,000=600,000,000 rows
which is about:
600,000,000/14,600,000,000≈4.1% of the table
If the same table were partitioned only by a user identifier (say, 500 buckets) instead, that same 30-day trend query has no time-based pruning at all, and has to scan across all 500 buckets' full history, effectively the entire 14.6 billion rows, because "the last 30 days" is not a property that partitioning scheme exposes to the query planner.
Trade-offs & pitfalls
Picking a high-cardinality key as the sole partition scheme breaks daily-append ergonomics and date-based retention, the two things a daily-loaded table usually needs most, in exchange for a join benefit that clustering could have delivered without that cost. Picking date alone when the workload is genuinely dominated by expensive user-level joins leaves real, measurable query cost on the table. Adding a hybrid scheme "for safety" without evidence of the join workload adds ongoing maintenance cost for no measured benefit; the decision should follow from which queries are actually expensive, not from habit or precaution.
Once a pipeline has multiple dependent stages, what does a workflow orchestrator actually give you that a plain cron job doesn't?
Sample Answer
Direct answer
A cron job only knows a clock time; it has no idea whether the upstream stage it depends on actually finished, failed, or is still half-written, so a multi-stage pipeline built on cron entries is really a set of independent guesses about how long each stage takes. A workflow orchestrator (Airflow, Dagster, Prefect and similar) models the pipeline as a directed acyclic graph (DAG) of tasks with explicit dependency edges, so a downstream task only starts when its upstream task actually reports success, and the system gives you retries, backfills, and one place to see the whole pipeline's health for free.
Structured elaboration
Dependency-awareness versus time-awareness. Cron triggers on wall-clock time alone. If stage two is scheduled 30 minutes after stage one, that gap is a guess about stage one's runtime, not a guarantee. An orchestrator's DAG edge says "task B runs after task A succeeds," which is true regardless of how long A actually takes that day.
Failure propagation. In a DAG, if a task fails, its downstream dependents simply do not run (or run a defined failure branch), automatically. With cron, each stage fires on its own schedule no matter what happened upstream, so a failed stage one silently lets stage two start against stale or partial data.
Retries with policy. Orchestrators let you attach a retry policy per task (bounded attempts, backoff), so a transient failure recovers without a human noticing. A cron job that fails just waits for its next scheduled fire, however far away that is.
Backfill and replay. Because tasks are parameterized by the DAG's logical run (a date or partition), you can rerun a single historical slice through the whole dependency chain. Cron has no concept of "this stage, for that date," so backfilling means manually re-invoking scripts in the right order.
Observability. A DAG-based orchestrator gives one UI or API surface for task duration, success/failure history, and where in the chain something is stuck. Cron gives you whatever each script happens to log, scattered across machines and cron logs.
When cron is still the right tool. A single, independent job with no downstream dependents and no need for backfill or retry policy does not need a DAG orchestrator; adding one there is pure overhead.
Worked example
Take a 3-stage pipeline: extract, transform, load. With cron, each stage gets a fixed offset:
extract=01:00,transform=01:30,load=02:00This assumes extract always finishes within 30 minutes. If the source is slow once and extract takes 45 minutes, transform still fires at 01:30 and reads a half-written file; cron has no signal that would stop it. Generalizing to a chain of N stages, cron needs N independently tuned fixed offsets, each one a manual guess that has to be re-tuned whenever runtime varies:
cron offsets needed=N,DAG dependency edges needed=N−1For N=5: cron requires 5 separately maintained time guesses; the DAG requires 4 edges, and those edges stay correct no matter how long any one stage takes, because the trigger is "the previous task instance succeeded," not a clock offset.
Retries make the gap sharper. Suppose a task fails transiently (a brief network blip to the source). An orchestrator retries with exponential backoff, say attempts at 1, 2, and 4 minutes after failure:
1+2+4=7 minutes until the task is resolved or escalatedA cron-based stage that fails has no retry concept built in: the next opportunity to reprocess is the next scheduled fire, often the next day:
24×60=1440 minutes before the same job runs againThat gap, 7 minutes versus 1440 minutes, is the practical cost of "no retry semantics," not an abstract one.
Trade-offs & pitfalls
An orchestrator is another system to run and keep healthy (unless you use a managed offering), so standing one up for a single independent job with no dependents is pure overhead with no payoff. Sensor or polling patterns (a task that waits by repeatedly checking for a file or a signal) can add their own latency or cost if the poll interval is misconfigured, so "dependency-aware" isn't automatically "instant." A common wrong turn is putting heavy transformation logic directly inside orchestrator tasks instead of having the orchestrator call out to dedicated compute; the orchestrator's job is to sequence and observe work, not to do the work itself.
What does the write-audit-publish pattern mean for a pipeline's data quality, and what problem does inserting an audit step before publish actually solve?
Sample Answer
Direct answer
Write-audit-publish means landing new data in a staging location consumers can't see yet, running data-quality checks (the audit) against it while it is still invisible, and only then making it visible with a single atomic operation (a partition swap or a pointer flip), rather than publishing first and validating afterward. The audit step exists to solve the problem of consumers reading bad or partial data during a load: without it, "load then check" means anyone querying during or right after a bad load already saw the wrong numbers before any check had a chance to catch it.
Structured elaboration
The problem this solves. A pipeline that writes directly into the table or partition consumers actually query, then validates afterward if at all, leaves a window between when writes start and when a check would have failed, during which consumers can read incomplete or wrong data with no visible signal anything is wrong.
The three stages:
- Write. Land the new batch in an isolated location, a staging table, a new partition or version, a new file set, that existing queries against the published location cannot see at all.
- Audit. Run validation against that staged data while it remains invisible to consumers: row counts against expectation, null rates, schema conformance, referential checks, business-rule checks.
- Publish. Only if the audit passes, flip visibility with a single atomic operation, an atomic partition swap, a metadata pointer update, or a table rename, so consumers see either the previous, already-audited state or the new, now-audited state, never a partial mix of the two.
Why the publish step has to be atomic. If publishing itself were multiple steps (remove old rows, then insert new ones), a reader querying in the middle could see an inconsistent partial state even after the audit already passed. The pattern only fully solves the visibility problem if the very last step is a single, indivisible flip.
What happens when the audit fails. The staged batch is simply never published. Production keeps serving the last good, already-audited state, and the failed batch goes to remediation instead of ever becoming visible to anyone.
Where it fits and where it doesn't. This suits batch or micro-batch loads into a location with many downstream readers who can't each validate before consuming, like a shared warehouse table. Strict low-latency streaming, where staging and swapping an entire batch isn't practical, achieves the same underlying goal (don't let one bad unit block or corrupt everyone) through per-record dead-letter handling instead.
Worked example
A daily table normally holds 2,000,000 rows. A join bug in that day's load causes fanout, and the staged batch lands with 2,600,000 rows:
2,000,0002,600,000−2,000,000=30% overcountIf the audit's row-count check allows a tolerance band of plus-or-minus 5 percent around the historical baseline, a 30 percent deviation fails it clearly, so the atomic swap never happens. Consumers keep reading yesterday's correct 2,000,000-row table while the bad batch sits quarantined and the join bug gets fixed, instead of every downstream report briefly, or permanently until someone happens to notice, reflecting a 30 percent inflated count.
flowchart LR
A[New batch written to staging] --> B[Audit checks run on staged data]
B -->|pass| C[Atomic publish: swap or pointer flip]
B -->|fail| D[Staged data quarantined]
C --> E[Consumers read new data]
D --> F[Remediation and retry]
F --> A
Trade-offs & pitfalls
- Auditing the already-published table instead of the staged one only catches the problem after consumers may already have read bad data; the ordering, audit before publish rather than after, is the entire point of the pattern.
- Making publish itself a multi-statement operation reintroduces the exact partial-visibility problem the pattern exists to prevent.
- The pattern trades some latency (data isn't visible until the audit finishes) and some storage (staged and published copies briefly coexist) for that safety; that's a clear win for a widely-consumed shared table, and a less obvious win for a low-stakes, single-consumer dataset where the extra latency may not be worth it.
- A common wrong turn is treating "we run some validation somewhere in the pipeline" as equivalent to write-audit-publish; the pattern specifically depends on the audited data being invisible to consumers until it passes, not just checked at some point along the way.
Design the monitoring and alerting for this pipeline: what would trigger a page versus a quiet log entry, and how would you avoid drowning the on-call engineer in noise?
Sample Answer
Direct answer
Reserve paging for issues that are both confirmed (sustained over more than one check, not a single blip) and actionable right now by the person being paged. Route everything else, especially first-occurrence anomalies and slow-moving trends, to a quiet log or dashboard reviewed on a cadence. Noise is controlled by comparing against rolling historical baselines instead of fixed numbers, requiring a breach to persist across several evaluation windows before it pages, and automating the remediations that are already well understood so a page is reserved for what actually needs a human decision.
Structured elaboration
The core decision:
| Question | Page (critical) | Log or dashboard only |
|---|---|---|
| Is the pipeline currently failing to deliver correct data (repeated hard failures, latency threatening the pipeline's service-level agreement (SLA), a dead-letter queue growing sharply)? | Yes | |
| Is it a single transient blip, a deviation the pipeline is expected to self-correct, or a slow drift that matters over days but isn't urgent right now? | Yes |
Noise-reduction techniques, and why each one earns its place:
- Baseline against rolling history rather than a fixed number. A static "must process more than X rows" threshold breaks the first time volume has a legitimate seasonal dip; a trailing baseline adapts with it.
- Require a sustained breach across several consecutive evaluation windows, not a single sample. One noisy reading crossing a line is common; requiring it to hold avoids paging on something that resolves itself before anyone even looks.
- Tier the thresholds (a lower bar for a non-urgent notification, a higher one for an actual page), so the person on call can tell "worth knowing" apart from "worth waking up for."
- Deduplicate and group related alerts. One upstream outage triggering separate alerts on every downstream consumer should collapse into a single page pointing at the shared root cause, not one page per symptom.
- Automate the remediations that are already well understood (retrying a transient failure, scaling up against a growing backlog), so a page is reserved for the cases a script genuinely can't decide on its own.
Escalation structure. A tiered path works best: a warning goes to a team channel, a sustained critical breach pages the on-call engineer directly, an unacknowledged critical page escalates to a secondary responder after a fixed window, and one still unresolved after a further window escalates again, to an engineering lead. This exists so an alert actively being worked doesn't independently keep escalating, while one that's genuinely stuck doesn't sit unowned indefinitely.
Worked example
Say the pipeline's hourly throughput baseline (a trailing 28-day average for that same hour of day) is 500 rows per second with a standard deviation of 50. Set the warning line at 2 standard deviations below baseline and the critical line at 3:
warning=500−2(50)=400 rows/sec critical=500−3(50)=350 rows/secRequire the warning to hold for 10 minutes and the critical for 5 before either fires. If observed throughput drops to 380 rows per second and holds there for 12 minutes, it has crossed the warning line (400) and stayed below it long enough to notify the team channel, but it hasn't yet reached the critical line (350), so it does not page. If it deteriorates further and holds below 350 for 5 minutes, it pages. The tiering keeps a real, sustained degradation from being ignored while not paging anyone at the very first dip below baseline.
flowchart LR
A[Metric vs rolling baseline] --> B{Breach severity and duration}
B -->|below warning| C[Log only]
B -->|warning, sustained| D[Team channel]
B -->|critical, sustained| E[Page on-call]
E -->|unacknowledged| F[Escalate to secondary]
F -->|still unresolved| G[Escalate to lead]
Trade-offs & pitfalls
- Paging on every metric deviation regardless of duration trains the on-call engineer to ignore pages; requiring a sustained breach costs a little detection latency in exchange for a page people actually trust.
- Static thresholds set once and never revisited go stale as the pipeline's normal volume changes; a baseline needs periodic review, not a permanent fixed number.
- Automating remediation for the wrong failure class (auto-restarting a job that's actually failing because of a genuine data problem, not a transient blip) can mask a real issue for a while and make the eventual page more confusing, not less.
- Mixing infrastructure-health alerts and data-quality alerts into the same tier is a common wrong turn; a resource-saturation page and a null-rate spike usually belong to different owners with different urgency, and combining them routes the wrong person to the wrong problem.
What is schema evolution (or schema drift), and why is it risky for a pipeline with many downstream consumers?
Sample Answer
Direct answer
Schema evolution is a deliberate, managed change to a data contract over time, such as adding a field or widening a type; schema drift is the same kind of change happening unmanaged, whenever a producer changes its output without coordinating with consumers. It is risky with many downstream consumers because each one decoded the old contract into its own assumptions (column order, types, required fields), and a change that looks harmless to the producer, such as renaming a field or tightening a type, can silently break, or worse silently mis-parse, every consumer nobody told.
Structured elaboration
Evolution, drift, and enforcement
| Meaning | Who controls it | |
|---|---|---|
| Schema evolution | A planned, compatible change to the contract, such as adding an optional field | The producer, coordinated with consumers through a shared contract |
| Schema drift | An unplanned change that happens anyway, such as a field's type quietly changing | Nobody, by definition, until someone notices |
| Schema enforcement | Rejecting data at the door if it does not match the expected contract | The pipeline's ingestion boundary |
Enforcement and evolution are complementary controls, not opposites: enforcement decides what is allowed to change at all, rejecting anything not covered by a known, versioned schema; evolution defines the compatible ways a schema is allowed to change without being rejected. A pipeline that only enforces, rejecting anything not byte-identical to the original schema, turns every legitimate producer change into an outage; a pipeline that only tolerates drift, accepting anything and adapting silently, makes every accidental change invisible until a consumer breaks downstream.
Why more consumers means more risk
Every additional consumer is another independent assumption about the contract, a required field it always expects, a type it parses a specific way, a column it reads by position. A change compatible with nine consumers can still break the tenth. Consumers rarely deploy in lockstep with a producer's change, so even a compatible change has a window where old and new consumers coexist and must both keep functioning against whichever schema version they actually receive. The failure mode is often silent rather than a crash: a numeric field that starts arriving as a string might get parsed as zero or null instead of raising an error, corrupting downstream aggregates without anyone noticing until the numbers look wrong.
Making evolution safe instead of just possible
Classify every proposed change as backward-compatible (old consumers can still read new data, such as adding an optional field with a default), forward-compatible (new consumers can still read old data), or breaking (neither), and require the first category for routine changes. Version the schema explicitly and require producers to register a change before shipping, so an incompatible change is rejected before it ever reaches a consumer, enforcement and evolution working together. Give consumers a tolerant-reading discipline, ignoring unknown fields and applying explicit defaults for newly added ones, so the registration rules are not the only thing standing between a schema change and a broken consumer.
Worked example
A shared events contract has a field that has always held a small, fixed set of string values. A producer team widens it to include a new value without registering the change. Consumers that validate against the original fixed set now reject or mis-bucket every event carrying the new value; consumers that do not validate at all just silently mis-categorize those records in downstream reports. Had the change gone through a compatibility check first, adding a value while the field stays the same type is backward-compatible for consumers that do not hard-validate the value set, but a known breaking change for the ones that do, the validating consumers could have been notified and updated before the producer shipped, instead of the team finding out from a support ticket.
Trade-offs & pitfalls
- Enforcing so strictly that every legitimate, backward-compatible change requires a coordinated multi-team release pushes teams to bypass the pipeline entirely for "small" changes, which just relocates the drift somewhere else.
- Relying purely on tolerant reading everywhere and skipping registration entirely lets drift stay invisible until an aggregate looks wrong, by which point the root cause could be weeks old.
- Backward-only compatibility is cheaper to enforce than full, both-direction compatibility, but it constrains rolling a change back, since old consumers reading new data is covered while new consumers reading not-yet-upgraded old data is not.
- Treating schema evolution as purely a technical versioning problem is a common wrong turn; the real risk is coordinating independently deployed consumers, which a version number alone does not solve.
Unlock Full Question Bank
Get access to all 38 Data Pipeline Architecture and Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.