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 handed a pipeline stage that isn't idempotent: rerunning it after a failure sometimes double-counts records downstream, and the sink itself doesn't support transactions. How would you redesign it to be safely retriable?
Sample Answer
Direct answer
When the sink itself cannot transact, move the "did this already happen" question out of the sink and into a small, dedicated record of what has been applied: assign each unit of work a stable, deterministic key, write to the sink, then record that the key has been applied, and have every retry check that record before writing again. The sink no longer needs to be transactional, because the safety property lives in the check-then-write pattern around it, not inside it.
Structured elaboration
The core pattern
Give every unit of work a deterministic key derived from its business identity, never a randomly generated per-attempt identifier, since two different attempts at the same logical work must produce the SAME key. Before writing to the sink, check an external record store for that key; if it is already marked applied, skip the write. After a successful write, mark the key as applied. Order matters here: the sink write happens first and the mark happens second, so if the process crashes in between, the worst case is one retried write plus a state check, not a lost write. Because the sink cannot transact, the record store around it only needs to guarantee a consistent "mark as applied" operation, which is a much smaller problem than making an arbitrary sink transactional.
Handling the gap between "sink write succeeded" and "record marked"
This is the one genuine edge case: if the process dies in that gap, a naive retry sees "not yet marked" and writes again, duplicating. Two workable responses, in order of preference. First, if the sink can absorb an idempotent write for that one operation on its own (an upsert keyed the same way), let it, and treat the external record store as an optimization that skips most redundant attempts rather than the last line of defense. Second, if the sink genuinely cannot deduplicate at all (pure append, no key), narrow the risk instead of eliminating it: keep the record-store update tightly coupled to the sink write, accept a bounded, rare duplicate-write risk in that specific crash window, and catch it later through reconciliation rather than trying to force an un-transactable sink into transactional behavior.
Folding in lineage for audit
Attach the same deterministic key, plus a timestamp and attempt count, to the record as it is written to the sink, not only to the external dedupe store. That turns the idempotency key into an audit trail as well: if a downstream consumer later asks whether a record was written once or might be a duplicate, the lineage metadata attached to the record answers it directly, instead of requiring someone to reconstruct the answer from a separate dedupe log that may have already expired.
flowchart LR
Producer[Upstream producer] --> Proc[Processor]
Proc --> Check{Dedupe key seen already?}
Check -- No --> Write[Write to sink]
Check -- Yes --> Skip[Skip: already applied]
Write --> Mark[Record key as applied]
Mark --> Sink[(Non-transactional sink)]
Worked example
A billing-events stage writes usage records to an append-only sink with no unique constraint. Redesign: assign each record a key built from the account identifier, billing period, and usage type. Before writing, check a small key-value record store for that key; if marked applied, skip. Write the record to the sink tagged with that same key as a lineage field. Mark the key applied in the record store. On retry after a mid-write crash, the check finds no record yet, so at most one duplicate line can appear in the sink for that key; a lightweight downstream step that keeps only the last-marked-applied row per key removes it before the data reaches reporting. This narrows the failure mode from unbounded duplication to one bounded, catchable case.
Trade-offs & pitfalls
- Putting all the safety into careful retries (backoff, fewer attempts) instead of into the write pattern reduces the odds of duplication without eliminating the mechanism that causes it.
- Keying on something that is not stable across retries, such as a freshly generated identifier per attempt, guarantees every retry looks like new work.
- The external record store adds a dependency and a bit of latency to every write; that is the cost of buying retry-safety for a sink that cannot provide it itself.
- Fully eliminating the crash-window duplicate is strictly better when the sink supports an idempotent absorb, but not every sink does; when it does not, the honest answer is a bounded, reconciled risk, not a claim of perfect exactly-once.
- Treating this as solved once retries stop producing visible duplicates in testing is a common wrong turn; the crash-window race is rare by construction and will not show up until production scale.
A pipeline produces several kinds of metrics: some feed financial reporting, others feed an approximate usage dashboard. Would you engineer exactly-once delivery everywhere, or only for some of these outputs? How do you decide, and what does it cost you where you don't?
Sample Answer
Direct answer
No: exactly-once delivery should be reserved for outputs where a duplicated or dropped event has real cost, financial reporting, billing, anything reconciled against an external source of truth, and skipped for outputs whose entire premise is being approximate, like a usage dashboard. The mechanics needed to guarantee exactly-once, idempotent or transactional sinks, checkpointed state, coordinated commits, cost throughput, latency, and operational complexity that buy nothing for a metric nobody is reconciling row by row.
Structured elaboration
The decision criterion. Ask whether the output is reconciled against something external, money, a contractual commitment, a compliance obligation. If yes, it needs exactly-once, or more precisely, at-least-once delivery paired with an idempotent sink, since true exactly-once delivery across a network doesn't really exist; what's achievable is at-least-once delivery combined with processing that behaves as if it were exactly-once. If the output is inherently approximate, or its own consumers already tolerate noise (a live "users active now" counter), at-least-once, or even best-effort delivery that drops under backpressure, is enough.
What exactly-once costs where you pay for it. Stateful checkpointing overhead, transactional or two-phase-commit sinks, and coordination between the stream processor's state and the sink's commit all add latency and reduce the maximum achievable throughput compared with a simpler at-least-once path.
What skipping it saves where you don't pay for it. Simpler at-least-once consumers, higher achievable throughput per unit of compute, and less operational surface, no transactional sink to keep healthy, because a slightly wrong dashboard number self-corrects on the next refresh and nobody downstream audits it row by row.
The cost of skipping it, made concrete. A duplicate or dropped event produces a bounded, usually small, percentage error in the approximate metric. That error has to be genuinely tolerable to the actual consumer, a rounding-scale display number, not a per-transaction financial line, for the trade to be sound. If the "approximate" dashboard has quietly become an input to a real financial decision, the trade was wrong regardless of how it was originally designed.
Ingestion technology selection. Which message bus is chosen constrains which guarantee is cheap to get, and interacts directly with each path's latency and ordering requirements. A bus with transactional producers and consumers and log compaction makes exactly-once-equivalent delivery straightforward for the financial path. A lighter at-least-once bus, or even a loss-tolerant, fire-and-forget transport for extremely high-volume telemetry, is the right, cheaper choice for the approximate path. Choosing one bus for both paths means either over-paying for guarantees the dashboard never needed, or under-provisioning the ordering and durability the financial path actually requires.
Worked example
Two output streams: financial (order revenue, about 50,000 events/day) and a usage dashboard (page-view pings, about 500,000,000 events/day). The financial path applies an idempotent-sink pattern keyed on order identifier; at 50,000 events/day the per-event bookkeeping overhead is cheap in absolute terms regardless of its per-event cost, because the volume is tiny.
Applying that same per-event dedupe pattern to the dashboard path would mean:
50,000500,000,000=10,000× the per-event bookkeeping work of the financial pathfor a number that's displayed rounded to the nearest thousand anyway. If the dashboard's display tolerates being off by up to 0.1%, that tolerance is:
500,000,000×0.001=500,000 events of slack per daywhich is far larger than the duplicate or drop rate an ordinary at-least-once pipeline actually produces (network-retry duplicate rates are typically a small fraction of a percent, well inside that budget). Paying the exactly-once tax on 10,000 times the event volume would buy correctness inside a margin nobody would ever notice.
Trade-offs & pitfalls
The common wrong turn is applying one delivery-semantics decision to the whole pipeline instead of per output: either over-engineering, exactly-once everywhere, paying an unnecessary latency and throughput cost on the high-volume approximate path, or under-engineering, at-least-once everywhere, silently double-counting revenue. Another pitfall is assuming "approximate is fine" without ever checking who actually consumes that number today; dashboards have a way of quietly becoming real decision inputs over time without anyone updating their delivery guarantees to match. Finally, "exactly-once" is used loosely enough that teams sometimes assume the message bus alone provides it and skip building the idempotent processing layer that's actually doing the work, then get duplicates in production the first time a consumer restarts mid-batch.
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.
A KPI turns out to be wrong. Walk through how you'd use lineage information to trace back through the pipeline and find which upstream table or transformation caused it.
Sample Answer
Direct answer
Start at the KPI's own defining table or view and walk its lineage graph upstream one hop at a time, using whatever lineage source is available (a transformation tool's dependency graph, a data catalog, or the warehouse's own query-history metadata) to list its immediate producers. Then prioritize which of those to inspect first by what changed most recently and which carries the most complex logic, rather than checking every upstream table with equal weight, and confirm a hypothesis by comparing actual numbers against historical baselines before calling it the root cause.
Structured elaboration
- Confirm the symptom precisely. Which number is wrong, since when, and by how much. The "since when" matters most, because it turns an open-ended search into "what changed upstream around that date."
- Pull the first-pass dependency graph from tooling, not from memory. A lineage tool, whether it's a transformation framework's dependency graph, a data catalog, or the warehouse's own lineage or query-history view, gives the KPI's immediate upstream tables and transformations in seconds. This should always be the first move, before reading any transformation logic by hand.
- Prioritize the candidates instead of sweeping all of them:
- Recency of change is the strongest signal; a code or schema change close to when the KPI diverged is the top suspect.
- Logic complexity matters next; joins, window functions, and aggregations hide subtle bugs far more often than a straight pass-through does.
- Recent operational incidents on a source (a known late or failed load) are an obvious, cheap first check.
- Validate quantitatively, not by inspection alone. Compare a suspect's current row counts, key distributions, or aggregate values against its own historical baseline for the same period. A real KPI bug shows up as a measurable divergence somewhere in the chain, and that comparison either confirms or rules out a candidate before more time is spent on it.
- Fix at the actual point of defect, not by patching the KPI layer to compensate; patching the transformation or coordinating with the upstream data owner, then re-running affected models forward, is what actually resolves it rather than hiding it.
- Add a targeted check to prevent recurrence on exactly the field or transformation that broke, so the same failure class is caught before it reaches the KPI again.
Worked example
Say the monthly revenue KPI comes in 8 percent below expectation for November, and the trace starts at the orders table feeding the revenue model. The typical daily order count in November is about 150,000. On November 14, the day the divergence first appears, the orders table shows only 122,000 rows:
150,000150,000−122,000=18.7% single-day dropSpread across a 30-day month, one day's 18.7 percent shortfall contributes roughly:
3018.7%≈0.62% to the monthly totalThat's far smaller than the 8 percent monthly miss actually observed, which rules out the single-day volume dip as the primary cause and points the trace toward a sustained, multi-day issue instead. Following the lineage graph one more hop, to the pricing table the revenue model joins against, turns up a schema change (a new discount field) that landed around the same time and caused the join to double-count discounted rows for every day the field has existed, a defect whose scale (spread across many days, not one) is consistent with an 8 percent sustained miss. The arithmetic above is what rules the first hypothesis out and justifies moving one hop further upstream, rather than stopping at the first plausible-looking suspect.
flowchart LR
A[KPI shows unexpected value] --> B[Pull lineage graph from KPI object]
B --> C[List immediate upstream sources]
C --> D[Prioritize by recency and logic complexity]
D --> E[Compare suspect vs historical baseline]
E -->|rules out| D
E -->|confirms| F[Fix at the actual source]
F --> G[Add targeted check to prevent recurrence]
Trade-offs & pitfalls
- Reading every model's transformation logic by hand before checking the automated lineage graph wastes time the tooling already answers in seconds; lineage-first is almost always the faster path.
- Checking every upstream table with equal priority, instead of ranking by recency and complexity, turns a targeted trace into an unfocused audit that takes far longer than it needs to.
- Patching the KPI view itself to compensate for a known-bad upstream input, instead of fixing the actual defective transformation, hides the bug until the next time that upstream table feeds something else.
- A common wrong turn is treating lineage as purely structural (what depends on what) without also checking when each dependency last changed; the timing correlation is usually what actually narrows the search from many candidates to one.
A validation check could either block bad records from moving further downstream, or just let them through and raise an alert. For a pipeline feeding a dataset other teams depend on, how do you decide which to do, and where in the pipeline would you put that check?
Sample Answer
Direct answer
Decide by weighing the cost of a false block (delaying or dropping a probably-fine record) against the cost of a false pass (letting a genuinely bad record reach every consumer of a shared dataset), not by a single fixed rule. Checks on a hard, structural guarantee that downstream logic actually depends on, like a required key being present or a value violating a constraint the rest of the pipeline assumes, should block. Checks that are really statistical health signals, like a metric drifting outside a normal range, should let the record through and raise an alert instead. Placement follows the same logic: put blocking checks as early as possible, before the data reaches anything shared, and put statistical checks wherever there's enough context to compute a meaningful baseline, which is often necessarily later.
Structured elaboration
Decision criteria, block versus alert-and-pass:
| Factor | Favors blocking | Favors alert-and-pass |
|---|---|---|
| Blast radius | Many downstream teams depend on this exact field or table | A single, low-stakes internal consumer |
| Nature of the violation | A hard, structural guarantee downstream logic assumes (non-null key, referential integrity, schema type) | A soft, statistical signal (an out-of-range value, a distribution shift) that could still be legitimate |
| Confidence in the check | Deterministic, unambiguous | Probabilistic or threshold-based, prone to false positives |
| Cost of being wrong | Reprocessing a false block is usually cheap once the pipeline can be trusted again | Letting through a false failure that's already mixed into a shared aggregate is often expensive or impossible to fully unwind |
Placement follows the same reasoning, not convenience. Hard, structural, cheap-to-evaluate checks belong as early as possible, ideally at ingestion, before a record can be joined, aggregated, or shared with anyone, because catching a violation before it's mixed into downstream state is strictly cheaper than unwinding it afterward. Statistical checks (drift, anomaly detection against a baseline) often need enough accumulated data to be meaningful, which can genuinely push them later in the pipeline; their outcome should still go to an alert rather than a block, since blocking on a probabilistic signal risks stalling the pipeline on a false positive.
A useful middle option: quarantine-and-continue. For a check that's borderline, confident enough to worry about but not confident enough to trust blindly, setting the record aside (not passed downstream, not silently dropped) while the rest of the batch proceeds avoids both a hasty block and a silent pass.
What kinds of checks tend to fall where. A null check on a required key or a duplicate-key check is usually the hard, deterministic, blocking kind. A range or distributional check on a numeric field is usually the softer, alert-worthy kind, precisely because it's inferring "unusual" rather than checking a fact the schema guarantees.
Worked example
A shared orders table feeds 12 downstream consumers. A schema check finds 0.4 percent of a day's 1,000,000 incoming records missing the required customer identifier, a value every downstream join assumes is present:
1,000,000×0.004=4,000 affected rowsEven at a small fraction of the batch, this is treated as blocking, because those 4,000 rows would otherwise silently break joins for all 12 downstream consumers at once, and the check itself is unambiguous (the key is either present or it isn't).
Separately, a statistical check on average order value shows a shift from a baseline mean of $85 to $102:
85102−85≈20% deviation from baselineThis could be a genuine promotion-driven spike or a data problem; it goes to alert-and-pass rather than blocking, because the check is probabilistic (not every deviation is a defect) and the cost of being wrong in the blocking direction (halting real data from a legitimate business event) is worse here than the cost of a few hours where a chart looks slightly off before a human confirms it either way.
flowchart LR
A[Raw ingestion] --> B[Hard structural checks: block on failure]
B -->|pass| C[Transform and aggregate]
C --> D[Statistical or drift checks: alert, let pass]
D --> E[Shared dataset]
E --> F[Downstream consumers]
B -->|fail| G[Quarantine, do not propagate]
Trade-offs & pitfalls
- Blocking on every check, including soft statistical ones, trades away data availability for a false sense of safety, and will eventually stall the pipeline on a legitimate anomaly like a real traffic spike.
- Letting every check through as alert-only, including hard structural violations, means a genuinely broken record (a missing key that breaks every downstream join) propagates to all 12 consumers before anyone even reads the alert.
- Placing every check right before a report renders catches problems far too late, after the data has already been joined and aggregated into shared state that's expensive to unwind.
- A common wrong turn is picking one block-or-alert policy for the whole pipeline instead of deciding per check, based on that specific check's own confidence and blast radius.
Unlock Full Question Bank
Get access to all 46 Data Pipeline Architecture and Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.