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 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.
Design a near-real-time dashboarding pipeline for product metrics that must keep event-ingestion latency under 5 seconds while sustaining 20,000 events/sec. Size the ingestion and processing layers, choose a storage layer for serving (fast OLAP vs. real-time store), and describe the caching strategy that gets read latency under a second for dashboard queries.
Sample Answer
Direct answer
At 20,000 events/sec with a sub-5-second ingestion latency target, the ingestion layer needs only a small number of partitions to handle the raw throughput comfortably, and the harder engineering problem is actually the read side: getting thousands of dashboard queries per second down to sub-second latency, which a caching layer in front of the serving store solves cheaply if the read pattern is repetitive (as dashboard traffic usually is).
Structured elaboration
Sizing the ingestion layer: with an average event size and a per-partition throughput ceiling (both stated assumptions here, since real limits depend on your specific broker and hardware), compute the number of partitions from both the message-rate cap and the raw-bytes-per-second cap, and provision for the larger of the two plus headroom for skew and growth.
Processing layer: a stream processor consuming those partitions, doing whatever light transformation/aggregation the metrics require, with parallelism matched to the partition count so no single consumer instance becomes a bottleneck. At this volume (about 19 MB/s), this is a modest cluster, not a large one; the sub-5-second latency bar is comfortably achievable without exotic tuning.
Storage layer for serving: choose a real-time-oriented store (a fast OLAP engine like ClickHouse/Druid, or a low-latency key-value/columnar store) over a traditional data warehouse for the serving path, since warehouses are optimized for large scans, not thousands of small, low-latency point/aggregate queries per second.
Caching strategy for sub-second reads: dashboard read traffic is highly repetitive (many viewers polling the same small set of metric keys), which makes it an excellent caching target. Put a cache (in-memory, short TTL matched to your freshness bar) in front of the serving store, so only a small fraction of reads (cache misses) actually hit the store, and even those hit a store designed for low-latency point reads.
Worked example
Partition sizing (assumptions: 1 KB average event size; a single partition sustains 10,000 msgs/sec or 10 MB/sec, whichever binds first):
EVENTS_PER_SEC, AVG_EVENT_BYTES = 20_000, 1_000
PER_PARTITION_MSG_CAP, PER_PARTITION_MB_CAP = 10_000, 10
throughput_mb_s = EVENTS_PER_SEC * AVG_EVENT_BYTES / (1024*1024) # 19.07 MB/s
partitions_by_msg_rate = -(-EVENTS_PER_SEC // PER_PARTITION_MSG_CAP) # ceil -> 2
partitions_by_mb_rate = -(-throughput_mb_s // PER_PARTITION_MB_CAP) # ceil -> 2
print(f"throughput_mb_s={throughput_mb_s:.2f}")
print(f"partitions_by_msg_rate={partitions_by_msg_rate}")
print(f"partitions_by_mb_rate={partitions_by_mb_rate}")
Executed output: raw throughput is 19.07 MB/s; both the message-rate and byte-rate caps independently require 2 partitions; provisioning 3 (50% headroom) comfortably absorbs skew and near-term growth without needing to repartition soon.
Cache sizing (assumptions: 500 distinct dashboard metric keys, 50 viewers per key, polling every 5 seconds, 98% cache hit rate, 150ms store read latency vs. 3ms cache read latency):
read_qps = 500 * 50 / 5 # 5,000 reads/sec, all viewers
store_qps = read_qps * (1 - 0.98) # 100 reads/sec actually hit the store
blended_latency_ms = 0.98*3 + 0.02*150 # 5.94 ms
print(f"read_qps={read_qps:.0f}")
print(f"store_qps={store_qps:.0f}")
print(f"blended_latency_ms={blended_latency_ms:.2f}")
Executed output: 5,000 total read QPS collapses to only 100 QPS actually reaching the serving store once the cache absorbs 98% of reads, and the blended expected latency across all reads is 5.94 ms, comfortably under a one-second target even though a single uncached store read (150ms) is far from instant on its own.
Trade-offs and pitfalls
The mistake to avoid is over-provisioning the ingestion layer out of an instinct that "20,000 events/sec sounds like a lot," when the arithmetic shows it's a genuinely modest throughput for a modern streaming platform; the real engineering effort here is on the read side, where naively hitting the serving store for every dashboard poll (5,000 QPS with no cache) would either require a much more expensive store or blow the sub-second latency target under load. The other pitfall is setting the cache TTL longer than the stated freshness requirement to squeeze out a higher hit rate; the cache's TTL has to be bounded by the same 5-second latency promise the whole system is built to keep, not chosen purely to optimize the cache-hit number.
A team needs both retrainable ML models (which want reproducible, accurate historical data) and low-latency online scoring. Which would you recommend, Lambda or Kappa, and why? Sketch how you would migrate from the other architecture with minimal risk, and describe how you'd keep the online features and the periodic batch snapshots used for training reconciled with each other.
Sample Answer
Direct answer
For a team that needs both retrainable models on reproducible historical data and low-latency online scoring, I'd lean toward a Kappa-style single streaming codebase with an explicit batch snapshot layer bolted on for training, rather than a full Lambda split, because the two consumers (the online scorer and the training pipeline) actually want the same underlying feature-computation logic, and duplicating that logic in a full Lambda setup is where ML teams most often introduce training-serving skew.
Structured elaboration
The key insight is that "low-latency scoring" and "reproducible training data" don't need two different processing engines, they need one feature-computation codebase and two different consumption patterns of its output: a live path for scoring, and periodic durable snapshots of the same computed features for training.
If choosing Kappa-leaning: run the feature computation as a single streaming job (Flink or Kafka Streams) that both serves online reads and periodically materializes a snapshot of feature values to durable storage (a feature store's offline layer or a warehouse table) at a fixed cadence. Training reads the snapshots; serving reads the live streaming state. Because both paths run the same code, a definition change (say, a new bucketing rule for a categorical feature) automatically applies to both, which is exactly the alignment you want.
Migration steps from a Lambda-style setup: (1) identify where the batch and speed layer logic has already diverged, and reconcile them into one shared definition first, before touching infrastructure; (2) stand up the streaming feature-computation job so it can both serve online reads and write periodic snapshots, running it in shadow (not yet serving) against production traffic; (3) validate that the streaming job's snapshots match the batch layer's historical output for a trailing window, within an agreed tolerance; (4) cut serving traffic over, keep the old batch layer running in parallel for a defined bake period as a fallback; (5) once validated, deprecate the separate batch feature pipeline.
Keeping online features and batch snapshots reconciled: version every feature definition (not just the values), so an offline snapshot can be tagged with exactly which code version produced it; run a scheduled comparison job that samples recent online feature values against what the batch snapshot computed for the same entities and time window, and alert if they diverge past a tolerance (some drift is expected from timing, not necessarily a bug, so the tolerance matters).
Worked example
A personalization model retrains nightly on nightly feature snapshots but scores online with sub-second freshness. Before migration, the team has two implementations of "user's 7-day click rate": a Spark job for training data and a Flink job for serving, and they've already seen the two disagree by a few percent because of a rounding difference in a time-window boundary. After migrating to the shared streaming codebase, the same Flink job computes the click rate for both serving and, once a day, snapshots it to the training table, so the discrepancy is structurally impossible rather than something the team has to keep re-verifying.
Trade-offs and pitfalls
The risk in this approach is treating the snapshot cadence as free: if training needs feature values reproducible for a specific historical instant (for point-in-time correctness, avoiding label leakage), the snapshot job has to be built with that discipline from day one, not bolted on later, since a naive "just query current state" snapshot will leak future information into training data. The other pitfall is skipping the shadow-validation step under time pressure; training-serving skew introduced silently during a rushed migration is far more expensive to debug after the fact than the extra week the validation step costs upfront.
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.
Compare the Lambda and Kappa architectures for combining batch and streaming processing: what components does each have, what operational complexity does maintaining two codepaths (Lambda) versus a single replayable streaming codepath (Kappa) actually cost you, and when would you choose one over the other?
Sample Answer
Direct answer
Lambda architecture runs two separate codepaths, a batch layer for accuracy and a speed layer for freshness, and merges their outputs at serving time. Kappa architecture collapses that into a single streaming codepath with full replay capability, using reprocessing instead of a second batch system to get accuracy. Lambda buys you a battle-tested batch layer as a correctness backstop at the cost of maintaining two implementations of your business logic; Kappa buys you one codebase at the cost of your streaming engine and storage having to support cheap, full-history replay.
Structured elaboration
Lambda's components: a batch layer (recomputes accurate views from the full historical log, usually on a schedule), a speed layer (a stream processor providing low-latency, possibly-approximate views of very recent data), and a serving layer that merges the two, typically overwriting the speed layer's view for a time window once the batch layer's more-accurate result for that window lands.
Kappa's components: a single durable, replayable event log (the source of truth) and one stream-processing codebase that both handles live traffic and, when you need to fix a bug or backfill, gets pointed at an earlier offset and replayed through the same code.
The real cost of Lambda is that your batch and speed layers are two independent implementations of the same business logic, in whatever language/framework each layer uses, and they can silently drift apart (a bug fixed in one and not the other, a metric defined slightly differently). That duplication is the actual argument against it, not raw complexity for its own sake.
The real cost of Kappa is that it pushes all correctness work onto the streaming engine: state management, exactly-once guarantees, and cheap full-history replay for potentially years of data, all inside one system, which is a harder engineering problem than "run a batch job over the same log."
Worked example
A team building fraud scoring that needs both a fast online signal and periodic full retraining on corrected historical labels is a natural Lambda fit: the speed layer scores in real time with best-effort features, the batch layer recomputes ground truth nightly once labels settle, and a small serving-layer reconciliation step swaps in the corrected value. A team building a single, well-defined metrics pipeline (say, per-minute active users) where the exact same logic should always apply, live or replayed, is a better Kappa fit: one Flink job computes it, and a bug fix just means replaying the log from the point of the bug, not fixing and re-deploying two systems.
Trade-offs and pitfalls
A common mistake is choosing Kappa because "one codebase" sounds obviously simpler, without checking whether the team's streaming engine and storage can actually afford cheap, full-history replay at their retention window and data volume; if replay is slow or expensive, Kappa quietly turns into "one codebase, but reprocessing takes three days," which defeats the purpose. The opposite mistake is defaulting to Lambda out of caution and then never actually keeping the two layers' logic in sync, which produces the exact dual-codepath drift Lambda is often criticized for.
Unlock Full Question Bank
Get access to all 9 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.