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.
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.
Downstream consumers occasionally see numbers shift after the fact because a small number of out-of-order records slip in after a window has already been published. Do you reprocess and silently correct the output, or publish a visible correction, and how do you decide which consumers even need to know?
Sample Answer
Direct answer
Default to correcting quietly only when the shift stays inside a window's already-communicated tolerance and nothing irreversible has happened downstream because of the old value; publish a visible correction whenever the change crosses a decision-relevant threshold, feeds an externally reported number, or has already been consumed by something that assumed the published value was final. The deciding factor is not how big the number moved, it is what already happened downstream because of the old one.
Structured elaboration
A decision framework, applied in order:
- Check the publication contract. Was the window published as final, or explicitly labeled provisional/subject to late adjustment? If the latter, a quiet correction within the window is expected behavior, not a surprise that needs an announcement.
- Measure materiality, not just magnitude. Does the corrected value cross a threshold that would change a decision (moves a KPI across a target line, reorders a ranking)? A small absolute change can still be material if the underlying baseline is small.
- Check what already consumed the old value. Has anything irreversible happened on the old number, a report already sent externally, a label already frozen into a training set, an automated action that already fired? If so, that consumer needs an explicit correction regardless of the value's size, because it cannot quietly re-read a value it already used.
- Segment consumers by how they read the data. Consumers that re-read the "current" value on every access (a live dashboard) self-heal automatically on the next read. Consumers that captured a value at a point in time (an export, a frozen label, a downstream aggregate that already rolled the old number into a larger sum) need an explicit correction, because they will not see the fix on their own.
Downstream-impact example: late-arriving or duplicate writes that silently "correct" an aggregate can also silently poison a machine learning training set. If a label, such as whether a user churned inside window W, is computed once and frozen into a training example, and a late-arriving event later reopens and changes what that label should have been, the model trained on that example now has a label that no longer matches production reality. Because the write that changed it was silent, nothing flagged the label as stale; the leakage does not show up in code review, it shows up later as an unexplained gap between offline and online model performance.
Worked example
An hourly-windowed metric ingests 1,000,000 events/day, roughly:
1,000,000/24≈41,667 events per hour
Suppose 0.1% of records typically arrive more than one window late:
0.001×1,000,000=1,000 late events per day, or roughly 1,000/24≈42 per hour-window
For a plain event-count metric, that same-size shift of about 42 events against a typical hourly count of 41,667 is under 0.1% of the total, likely below any reasonable materiality threshold, so a quiet correction is defensible. But if those same 42 late events are concentrated in a rare category, say a fraud-flag count that normally sits at 5 per hour, then +42 late flags is roughly an 8x change to that specific number:
(5+42)/5=9.4× the original value
which is clearly material, even though it is the exact same underlying late-arrival rate. The correction decision has to be evaluated per metric, not off one global "percent of total volume" rule.
Trade-offs & pitfalls
Over-notifying on every minor correction trains consumers to ignore correction alerts, the same alert-fatigue failure mode as over-alerting on any monitoring system. Under-notifying breaks trust the first time someone discovers a number quietly changed underneath them without ever being told. Treating percent-of-total-volume as the sole materiality signal, as the worked example shows, misses low-base-rate metrics where a small absolute shift is proportionally enormous. And "silent" is only actually safe when nothing irreversible has consumed the old value yet, silently overwriting a value that has already been exported, reported, or frozen into a training label does not undo that consumption, it just hides that it happened.
Design where records that fail parsing or validation go in this pipeline, and how you'd get them corrected and safely replayed back into the main flow later.
Sample Answer
Direct answer
Records that fail parsing or validation go to a dead-letter queue (DLQ), a separate durable holding area, tagged with the original payload, the specific error, and a stable identifier tying them back to their place in the source. Correction happens outside the main flow (automated repair for known-fixable patterns, manual remediation for the rest), and replay pushes the corrected record back through the exact same validation gate it failed under a new attempt identifier, so it either passes cleanly or fails again visibly. Nothing is ever silently patched and slipped back in.
Structured elaboration
Put the gate as early as possible. Validation belongs at the ingestion boundary, before a record reaches any join, transform, or aggregation stage. Catching a bad record before it's mixed into shared state is cheap; unwinding an aggregate it already corrupted is not.
Parsing failures and validation failures are different problems. A parsing failure (malformed structure, broken encoding) usually points at a producer bug and is rarely safe to auto-fix. A validation failure (a missing required field, a value out of range, a reference that doesn't resolve) is often fixable given a bit more context, which is why it's worth routing these two differently rather than treating every failure the same way.
What has to travel with a record into the DLQ. The raw original payload, the specific rule or field that failed, a timestamp, a stable identifier that preserves its position in the source stream, and a retry or attempt counter. Without this, remediation is guesswork.
Two correction paths, not one. A known, safe failure pattern (for example, a schema version the pipeline hasn't rolled out support for yet) can go through an automated repair-and-resubmit step with no human involved. Everything else goes to a remediation surface where a person can see the failure reason and the payload, fix or annotate it, and mark it ready to go back through.
Replay through the same gate, under a new identity. A corrected record is resubmitted through the identical validation logic it originally failed, not around it, so a bad fix still gets caught. It carries a new attempt identifier rather than overwriting the original entry, preserving an audit trail of what failed, what changed, and when it was retried.
Watch the queue itself, not just individual failures. A DLQ that only ever grows is itself a signal. The right response to a small, steady trickle (normal background noise) is different from the right response to a sudden spike (usually a systemic break, like an upstream schema change), and the pipeline needs a way to tell those apart rather than routing an ever-growing stream to a queue nobody is watching.
Worked example
Say the pipeline ingests 2,400,000 records a day, and historically 0.25 percent fail validation:
2,400,000×0.0025=6,000 records/day to the DLQSpread evenly across 24 hours, that's a baseline arrival rate of:
246,000=250 records/hourNow suppose an upstream schema change causes 8 percent of one hour's arriving records (100,000 that hour) to fail validation:
100,000×0.08=8,000 records in that single hourCompared to the 250-per-hour baseline, that's:
2508,000=32× the normal hourly rateIf the alerting rule fires whenever the DLQ arrival rate exceeds 5 times the trailing baseline, this spike (32 times baseline) triggers it clearly, which is exactly the signal that should prompt someone to check for a systemic cause, like a schema change, instead of assuming it's more of the same background trickle.
flowchart LR
A[Incoming record] --> B[Validation gate]
B -->|passes| C[Continue through pipeline]
B -->|fails| D[Dead-letter queue]
D --> E[Automated repair, known patterns]
D --> F[Manual remediation]
E --> G[Replay, new attempt id]
F --> G
G --> B
Trade-offs & pitfalls
- A DLQ with no remediation and replay loop is a write-only graveyard; it's no better than silently dropping the records, just more expensive to store.
- Reprocessing a corrected record under its original identity, instead of a new attempt identifier, erases the audit trail of "this failed once, here was the original data."
- Auto-retrying validation failures that are genuinely permanent (a business-rule violation, not a transient glitch) wastes effort on records that will fail identically every time until the rule or the data changes.
- Placing the validation gate after joins or aggregation means a bad record has already corrupted shared downstream state before anyone catches it; moving the gate earlier is nearly free, unwinding a corrupted aggregate is not.
- Treating every spike in DLQ volume as "more of the same" instead of a signal worth investigating is how a schema break goes unnoticed for hours.
A single stream of database change events needs to reach several very different downstream systems: a cache, a search index, and a warehouse. How would you fan that out, and what has to be true for each downstream system to stay consistent with the source?
Sample Answer
Direct answer
Publish the change stream once, through change-data-capture (CDC, a technique that captures row-level inserts, updates, and deletes from a source database as a stream of events) into a single durable, ordered log partitioned by primary key, and let the cache, the search index, and the warehouse each consume that log independently through their own idempotent apply logic. Consistency here does not mean all three agree at every instant; it means each one correctly converges to the source state once it has processed everything up to some point, at a staleness bound that fits what that system is actually used for.
Structured elaboration
One producer, many independent consumers. Capturing the change stream once and fanning it out through a shared log avoids coupling the three downstream systems to each other or to the source's write path; a slow search indexer never blocks the cache, and a cache outage never blocks the warehouse load.
Ordering is per-key, not global. Partitioning the log by primary key keeps every event for a given row in order relative to each other, which is the guarantee that actually matters. There is no practical way to guarantee a single global order across every row in the table at this scale, and none of the three downstreams need one.
What has to be true for each downstream, and why it's different:
| Downstream | What "consistent" means for it | How it stays that way |
|---|---|---|
| Cache | Serves live reads, so staleness has to be small; only the latest value per key matters, not history | Apply as a conditional write: only overwrite if the incoming event's version is newer than what's stored, so an out-of-order redelivery can't roll a key backward |
| Search index | Tolerates a few seconds to minutes of lag; needs the latest document state, not full history | Apply as an upsert keyed by document id with the same newer-than-stored version check; deletes become tombstones rather than silent removals |
| Warehouse | Tolerates minutes to hours of lag; often needs the full history of changes, not just the latest state | Apply every event, deduplicated by a stable event identifier or offset, typically in batches rather than continuously |
Idempotency is not optional for any of them. Because delivery from the log is normally at-least-once (a consumer can see the same event again after a restart or a retry), every downstream apply logic has to be safe to run twice, which is why the version or offset check matters more than the transport's own guarantees.
Schema tolerance matters as much as ordering. A column added upstream should be ignorable by a consumer that doesn't know about it yet; a column removed upstream should degrade to a default rather than break parsing. A consumer that can't tolerate either will silently drop or misparse records the moment the source schema changes, which is a common and quiet way "consistency" breaks in practice.
Eventual is not the same as guaranteed. Because each consumer applies independently and can lag or briefly misbehave, every downstream needs its own periodic reconciliation, comparing row counts or checksums against the source, rather than trusting the stream to have delivered everything correctly forever.
Worked example
Suppose the change log has 12 partitions and the source sustains 6,000 change events per second, evenly spread, so each partition carries:
126,000=500 events/sec per partitionIf the search-index consumer degrades and can only index 400 events per second per partition, each partition accumulates a backlog of:
500−400=100 events/secAfter 5 minutes (300 seconds) of that degraded rate, each partition has queued:
100×300=30,000 eventsAt the degraded processing rate of 400 events/sec, working through that backlog takes:
40030,000=75 secondsSo at that moment the search index is roughly 75 seconds behind the source, and the gap keeps growing as long as the consumer stays degraded, which is exactly the kind of staleness number a team would want on a dashboard for this specific downstream, since 75 seconds might be fine for search but would already be a problem for the cache.
flowchart LR
A[Source database] --> B[CDC capture]
B --> C[Partitioned log, keyed by primary key]
C --> D[Cache sink]
C --> E[Search index sink]
C --> F[Warehouse sink]
D --> G[Reconciliation vs source]
E --> G
F --> G
Trade-offs & pitfalls
- Treating "consistency" as one uniform target across all three sinks is the most common wrong turn: a warehouse can tolerate hours of lag, a cache serving live reads cannot tolerate more than seconds without visibly confusing users.
- Writing to all three sinks synchronously from the source, instead of fanning out from one shared log, couples the source's write latency and availability to the slowest downstream and removes any ability to retry one sink in isolation.
- A schema change is the most common cause of a silent consistency break; a consumer that isn't tolerant of an added or removed field can drop or misparse records with no visible error.
- Assuming ordering holds across the whole stream, rather than just per key, leads to logic that quietly breaks the first time two different keys' events are legitimately processed out of relative order.
- Skipping periodic reconciliation because "the stream should be reliable" is how silent drift goes unnoticed for weeks; every downstream needs an independent way to detect that it no longer matches the source.
What should go into a data contract between a team that produces a dataset and the teams that consume it, and how would you actually enforce it rather than just document it?
Sample Answer
Direct answer
A data contract needs three layers: what the data looks like (schema, types, semantics), what service level the producer promises (freshness, availability, and a compatibility policy for future changes), and who is accountable when either breaks (ownership and escalation path). Enforcement means those layers are checked automatically at the moment a break would actually happen, before a producer ships a change and continuously against what is actually being served, rather than living only in a wiki page that nobody re-reads under deadline pressure.
Structured elaboration
| Contract element | Contents |
|---|---|
| Schema and semantics | field names, types, nullability, units, allowed value ranges, enum meaning, canonical serialization format |
| Service levels | freshness (maximum data age), availability, latency and throughput bounds |
| Compatibility policy | what counts as an additive versus a breaking change, and the required deprecation notice window for breaking ones |
| Ownership | producer and consumer contacts, on-call path, escalation route |
Enforcement mechanisms, focused on when each one fires:
- Schema registry gate: the producer's build must pass a compatibility check against the registered schema before a change can ship, catching a breaking change before production, not after.
- Runtime validation: ingestion rejects or quarantines records that violate the contract instead of letting them flow through as silent nulls or coerced types.
- Consumer-driven checks: tests that fail the producer's build if a real consumer's stated expectations would break, so a producer is not only checking its own schema in isolation.
- Staged rollout of enforcement itself: new contracts start in alert-only mode and tighten to blocking mode over time, so enforcement does not halt unrelated work the first day it exists.
Worked example
A catalog team produces a shared product dataset (product_id, name, price, category, currency), consumed by a pricing service, a search index, and a recommendation pipeline. The contract specifies that price is always an integer in minor units (cents), category is one of a fixed enum, and only additive changes ship without notice, a renamed or removed field requires two weeks' notice. Enforcement: the catalog team's build runs a schema-compatibility check against the registry before any migration ships. If a change tries to remove category or switch price from integer cents to a float dollar amount, that check fails the build immediately, because both are breaking changes the contract explicitly covers, catching the problem before the recommendation pipeline (which assumes integer cents) would have silently mispriced results for every consumer downstream.
Trade-offs & pitfalls
Writing the contract as documentation only, without the registry and build-time checks, means it decays the first time someone ships in a hurry; a contract nobody's tooling reads is a contract nobody actually follows under pressure. Enforcing everything in blocking mode from day one, on an organization with no existing baseline compliance, halts unrelated work and burns trust in the contract itself; starting in alert-only or pilot mode and tightening over time avoids that. A contract that only covers schema shape and ignores semantics, a currency or unit change that does not change the field's type, can pass every schema check and still silently break every consumer that assumed the old meaning.
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.