Batch, Streaming, and Real-Time Serving Trade-offs Questions
Reasoning about when to use batch, micro-batch, or continuous streaming and how to serve low-latency analytics: latency, cost, complexity, and correctness trade-offs; lambda vs kappa architectures; and reprocessing semantics. Covers real-time aggregation, freshness vs consistency trade-offs, and reconciling streaming results with batch ground truth, including geospatial and high-throughput real-time workloads under eventual consistency. The data-systems judgment topic for choosing and reconciling batch versus real-time approaches, distinct from the hands-on streaming transport itself.
Your streaming aggregation for daily totals disagrees with the nightly batch totals by 2% for the last month. The streaming pipeline uses a 5-minute watermark and 10 minutes of allowed lateness. Walk through an investigation and remediation plan: how would you quantify the impact of late events on the discrepancy, and how would you correct the historical daily totals once you know the cause?
Sample Answer
Direct answer
A consistent 2% streaming-vs-batch shortfall with a fixed 10-minute allowed-lateness window is the classic fingerprint of the watermark dropping late events the batch layer still counts, so the investigation should start there before assuming a bug, and the fix is either widening the lateness window or explicitly reconciling the dropped tail, not rewriting the aggregation logic.
Structured elaboration
Step 1, quantify the late-event distribution: pull the true arrival-time-versus-event-time delta for a sample of production events from the raw log (not from the streaming job's output, which has already discarded what it dropped). If a meaningful fraction of events arrive more than 10 minutes after their event time, that's a sufficient explanation for a shortfall, and the size of that fraction should roughly match the 2% gap.
Step 2, confirm the mechanism, not just correlate it: rather than assuming, build the arithmetic. If X% of daily events arrive later than the 10-minute allowed-lateness window, the streaming aggregate will structurally be short by approximately X% (modulo any events that arrive within the window and are still counted). This step (below) shows a concrete, reproducible version of that check.
Step 3, remediate: two independent levers, not mutually exclusive: (a) widen the allowed-lateness window if the operational cost (more held-open state, slightly higher latency before a window finalizes) is acceptable, which recovers some of the gap directly; (b) accept that some tail will always arrive later than any reasonable window and build an explicit reconciliation step that corrects the streaming-derived historical numbers against the batch recompute on a schedule, rather than treating the streaming number as ever being the final one for past periods.
Step 4, correct history: for the past month's data, don't try to "fix" the streaming aggregate retroactively; simply replace the affected historical daily totals with the batch-recomputed values (which, since batch sees the full late tail, are the accurate ones), and communicate clearly to downstream consumers that historical numbers for that period were corrected and why.
Worked example
A synthetic reproduction of the mechanism, with all inputs pinned and seeded:
import random
random.seed(7)
N_EVENTS = 200_000
ON_TIME_FRACTION = 0.965
n_on_time = int(N_EVENTS * ON_TIME_FRACTION)
n_late = N_EVENTS - n_on_time
# late events: minutes-late drawn from an exponential with mean 25 minutes
late_minutes = [random.expovariate(1 / 25) for _ in range(n_late)]
allowed_lateness_minutes = 10
late_but_within_window = sum(1 for x in late_minutes if x <= allowed_lateness_minutes)
streaming_count = n_on_time + late_but_within_window
shortfall_pct = (N_EVENTS - streaming_count) / N_EVENTS * 100
print(f"n_on_time={n_on_time}")
print(f"n_late={n_late}")
print(f"late_but_within_window={late_but_within_window}")
print(f"streaming_count={streaming_count}")
print(f"shortfall_pct={shortfall_pct:.2f}")
Executed output: 200,000 total events (batch ground truth); 193,000 on-time; 7,000 late (of which 2,380 arrive within the 10-minute window and are still counted, 4,620 arrive later and are dropped); streaming count = 195,380; shortfall = 4,620 events = 2.31% of the batch total. This is a synthetic reproduction (not the actual production event-time histogram), but it demonstrates that a late-arrival distribution with roughly this shape, about 3.5% of events arriving late with a mean lateness of 25 minutes against a 10-minute window, is fully sufficient to produce a ~2% streaming shortfall on its own, with no bug required. A real investigation would replace this synthetic distribution with the actual measured one from step 1 and confirm the numbers line up.
Trade-offs and pitfalls
The biggest mistake here is assuming a discrepancy this size must be a bug and going straight to a code review of the aggregation logic, when the far more common cause (a mismatch between the allowed-lateness policy and the real-world late-arrival distribution) is cheap to check first and, in this case, fully explains the gap. The second mistake is widening the lateness window as a blanket fix without checking the cost: every extra minute of allowed lateness means every window stays open longer, holding more state and delaying when downstream consumers can treat a number as final, so the right lever depends on how much that delay actually costs versus how much of the 2% gap it recovers.
A client needs dashboards with near-real-time analytics. Design a hybrid architecture that delivers low-latency slices where they matter and cheaper batch aggregates everywhere else, while minimizing duplication of transformation logic between the two paths. Explain how you keep the two paths from silently disagreeing, and how you'd resolve it when they do.
Sample Answer
Direct answer
The way to deliver low-latency slices without duplicating transformation logic is to run one shared transformation codebase (via a framework that can execute the same logic in both a streaming and a batch runner, like Apache Beam, or by hand-sharing a well-tested library between a stream job and a batch job) and let only the serving layer decide which path's output to show for a given time window, favoring the streaming path for recent data and swapping in the batch path's result once it's available.
Structured elaboration
Architecture: a streaming job computes low-latency, best-effort aggregates for the last few hours; a batch job recomputes the same aggregates, exactly, once a day, over the full corrected data. Both call into the same shared transformation logic (the aggregation rules, filters, and business definitions live in one place), so there is no second, independently-maintained definition of "revenue" or "active users" to drift.
Serving-layer merge: for any given time bucket, the serving layer shows the streaming result until the batch job has produced a reconciled result for that bucket, then swaps to the batch result. This is the same core idea as a Lambda architecture, but scoped narrowly, only the two output values for the same window, not two independently-designed pipelines.
Keeping the two paths from silently disagreeing: because both paths execute the same shared logic, most disagreement sources collapse to one thing, event-time handling (how long the streaming path waited for late data before finalizing a window versus the batch path, which by definition saw everything). Make that explicit: tag every value the serving layer returns with whether it's "provisional" (streaming, still within the batch job's not-yet-processed window) or "final" (batch-confirmed), so a disagreement is visible and expected rather than silent.
Resolving it when it happens: when the batch result for a window differs materially from what the streaming path showed, log the delta (don't just silently overwrite), and if the delta exceeds an agreed threshold, that's a signal worth investigating (a bug in the shared logic's event-time handling, an unusually long tail of late data), not just something to paper over.
Worked example
A client-facing dashboard shows the last hour's revenue from the streaming path, tagged "updating", and prior hours from the batch path, tagged "final". If the streaming path showed $48,200 for 2-3pm and the batch reconciliation later computes $48,650 for the same window (a 0.9% difference from late-arriving refund records), the dashboard swaps in $48,650 once available, and because both numbers came from the same underlying aggregation logic, the team can be confident the 0.9% gap is genuinely explained by late data, not by two different definitions of revenue disagreeing.
Trade-offs and pitfalls
The common failure mode is building the streaming and batch paths as two separate implementations "to move faster," with the intention of unifying them later, which almost never happens under deadline pressure and results in exactly the silent-drift problem this design exists to prevent. The other pitfall is hiding the provisional/final distinction from end users to make the dashboard feel simpler; that trade always eventually backfires the first time someone notices a number changed and nobody can explain why, because the system gave them no signal that it was expected.
Design a hybrid pipeline where nightly batch jobs compute historical values and a streaming job updates recent values on top of them. How do you reconcile the overlap between the two (streamed recent updates vs. batch historical values), prevent double-counting once the batch job catches up, and ensure the combined result is monotonic and reproducible?
Sample Answer
Direct answer
The key design decision is a clean, well-defined handoff boundary: the streaming path owns everything newer than a fixed cutoff (say, the last 24-48 hours), the batch path owns everything older, and every value in the combined output is tagged with which path produced it, so "double counting" becomes structurally impossible rather than something you have to detect after the fact.
Structured elaboration
The handoff boundary: pick a cutoff time window (based on how long your data typically takes to fully settle, including late arrivals and any correction processes) before which data is considered "batch-owned." The streaming path only ever writes to the window newer than that cutoff; the nightly batch job only ever (re)writes to the window at or older than the cutoff. Neither path writes into the other's territory, which is what actually prevents double-counting, rather than trying to deduplicate after merging two overlapping outputs.
Reconciling overlap: as the cutoff advances each day (yesterday's "streaming-owned" data becomes old enough to be "batch-owned" today), the batch job recomputes that newly-crossed-over window from the full, settled data and overwrites whatever the streaming path had written for it, rather than appending to it. This overwrite, not merge, is what keeps the combined table free of duplicates.
Monotonicity and reproducibility: because the batch job always fully recomputes (and overwrites) its owned windows from the immutable source log, rerunning it for the same historical window always produces the same result, which is what "reproducible" means here. Monotonicity (values only get more correct over time, never inconsistently jump around) follows from the batch job being the sole writer once a window crosses the cutoff; the streaming path never touches that window again.
Implementation detail that matters: this requires the storage layer to support an efficient overwrite-by-partition operation (most warehouse and lakehouse formats do, via a partition-level replace or a MERGE keyed on the time window), not an append-only log, otherwise "overwrite" becomes an expensive full-table rewrite.
Worked example
With a 24-hour cutoff: today's data (0-24 hours old) is served entirely from the streaming path's continuously-updated aggregate. Tomorrow, once today's data is fully settled, the nightly batch job recomputes today's aggregate from the raw log and overwrites the partition the streaming path had been writing, at which point that partition is permanently batch-owned and the streaming path stops writing to it. A user querying "yesterday's total" always gets the batch-recomputed, final value; a user querying "today's total" always gets the streaming path's continuously-updating value, and the two never both contribute to the same partition.
Trade-offs and pitfalls
The common mistake is designing this as an append-and-deduplicate system (both paths write, then a dedup step tries to reconcile duplicates by, say, keeping the most recent write per key), which is fragile: dedup logic itself can have bugs, and it obscures which value is authoritative at any given moment. The partition-ownership-by-time-window design avoids that whole class of bug by construction. The remaining risk is choosing a cutoff that's too short for how long your data actually takes to settle, in which case the batch job will be overwriting a partition that hasn't fully arrived yet, reintroducing exactly the streaming-vs-batch discrepancy this design is meant to eliminate; the cutoff should be set from measured late-arrival data, not a round number picked by convention.
Design a near-real-time feature store that supports both offline batch features and online features with a 1-minute freshness target. How do you reconcile the two, so a value computed by the nightly batch job and the same value computed by the online path don't silently drift apart, and how do you version features as the reconciliation logic changes?
Sample Answer
Direct answer
The way to keep an offline batch-computed value and an online streaming-computed value from silently drifting apart is to make them provably the same computation, not two independently-tuned approximations of the same idea, by sharing the feature definition's code (or at minimum its exact specification) between both paths and continuously measuring the gap between them rather than assuming it's zero.
Structured elaboration
Reconciliation mechanism: implement the feature's transformation logic once, in a form that both the offline batch job and the online serving path can execute (a shared library, or a framework that compiles one definition to both a batch and a streaming runner). This removes the most common cause of drift, which is two engineers implementing "the same" feature slightly differently (different time-window boundary conventions, different null-handling, different rounding).
Feature versioning: attach a version identifier to every feature definition, and store that version alongside every computed value, both online and in the offline snapshot table. This lets you answer "which exact logic produced this number" months later, and lets you roll out a definition change safely (both paths read the new version's code, and old data stays tagged with the old version rather than being silently reinterpreted).
Measuring drift, not assuming it away: run a scheduled job that samples recent entities, pulls both the online-computed value and what the offline batch snapshot computed for the same entity and time, and reports the distribution of differences. Some non-zero drift is expected and fine (timing differences mean the online value reflects slightly more recent data than the batch snapshot captured), but the monitoring job should alert if the drift distribution shifts meaningfully, which usually signals a real bug (a definition mismatch, a bad deploy) rather than expected timing noise.
Handling the reconciliation logic changing over time: when the feature definition itself changes (a new bucketing rule, say), version the change explicitly, backfill historical offline snapshots under the new version if training needs consistent history, and don't silently apply the new definition to old data without re-versioning it, since that would make the drift-monitoring job (and any model trained on the old data) unable to tell what actually changed.
Worked example
A "user's 7-day purchase count" feature is defined once, in a shared library, and compiled to run both inside the online Flink job (serving reads) and the nightly batch snapshot job (training reads). The drift-monitoring job samples 10,000 users daily, compares online vs. snapshot values for the same user and day, and typically finds a small, explainable gap (the online value sometimes includes a purchase from the last few minutes that the prior night's snapshot didn't yet have). When a deploy accidentally changes the online path's window boundary from inclusive to exclusive, the drift-monitoring job's next run shows a sudden, systematic shift in the difference distribution (not just noise), which is exactly the signal that catches the bug before it silently degrades whatever model consumes the feature.
Trade-offs and pitfalls
The most common mistake is building the online and offline computations as two separate implementations "because they're different systems anyway" and trusting code review to keep them aligned; code review reliably misses subtle semantic differences (an inclusive versus exclusive window boundary looks identical in two different codebases written by two different people). The second mistake is treating any measured drift as automatically a bug and chasing it to zero; some drift is structurally expected from timing alone, and the useful signal is a change in the drift distribution, not its mere existence.
Your team processes 100M events/day through a batch pipeline and retrains models every few hours. Someone proposes adding real-time features for personalization. Before committing to that, how would you evaluate whether the need is real: what minimal experiment or prototype would you run, and what objective success and cost criteria would decide whether it's worth building?
Sample Answer
Direct answer
Before building anything, I'd run a cheap, time-boxed experiment (a data-only prototype, not a real streaming pipeline) that estimates the upper bound of value real-time features could add, using data the team already has, and only commit to the real build if that upper bound clears a cost threshold worth the engineering investment.
Structured elaboration
The minimal experiment: take a sample of historical events, and instead of building a live streaming pipeline, simulate what a real-time feature would have looked like by re-deriving it from the existing batch data at fine time granularity (for example, reconstructing "user's activity in the last 5 minutes" from timestamped logs offline). Feed that simulated feature into an offline evaluation of whatever downstream system would use it (a recommendation model, a personalization rule), and measure the lift over the current nightly-batch-derived feature. This gives a real signal about value without paying for any streaming infrastructure.
Why this bounds the value from above: a production real-time pipeline will always be somewhat noisier and lossier than a perfectly-reconstructed offline simulation (late data, partial state on cold start, occasional gaps), so if the simulated version doesn't show a meaningful lift, a real implementation won't either, and you've avoided the build entirely.
Objective success criteria: define the lift threshold before running the experiment, not after seeing the result, ideally in terms the business cares about (a lift in a downstream metric like conversion rate or session length, not just a proxy metric like feature correlation). Also define a cost threshold: estimate the ongoing infrastructure and operational cost of a real streaming build (compute, on-call burden, added system complexity), and require the measured lift to translate into more business value than that ongoing cost, not just be statistically distinguishable from zero.
Worked example
Suppose the offline simulation shows that using 5-minute-fresh features instead of the current several-hours-old batch features would lift a downstream conversion metric by an estimated 0.3 percentage points on a baseline of 4%, a real but modest improvement. If that lift, extrapolated across current volume, is worth less per month than the estimated cost of running the streaming infrastructure to get 5-minute freshness in production, that's a clear "not yet" answer, and the team should revisit only if volume grows enough to change that math, or if the freshness need becomes strategic rather than incremental.
Trade-offs and pitfalls
The main pitfall is running the offline simulation but skipping the cost side of the comparison, which tends to happen when the team is technically excited about streaming and only wants to prove the value case, not weigh it. The other pitfall is picking a lift threshold that's too easy to clear (any statistically significant lift, however tiny) rather than one tied to whether the lift is large enough to justify the ongoing operational cost; a real-time feature that's measurably better but not meaningfully better is a bad trade once you account for what running it in production actually costs.
Unlock Full Question Bank
Get access to all 21 Batch, Streaming, and Real-Time Serving Trade-offs interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.