ML Feature Pipelines and Feature Stores Questions
Data infrastructure for machine learning: feature pipelines, feature stores, online/offline consistency, training-serving skew, and data preparation for models. Covers building reliable feature platforms and preventing leakage in the data path feeding models. The data-engineering-for-ML topic.
Explain schema evolution: what it is, why it matters for feature pipelines, and how commonly used serialization formats (Avro, Parquet, Protobuf) support it. Describe a process for handling a breaking schema change in a production streaming pipeline that has multiple downstream consumers.
Sample Answer
Direct answer: Schema evolution is the practice of changing a data schema over time (adding, removing, or modifying fields) while keeping existing producers and consumers working, and formats like Avro, Parquet, and Protobuf support it through explicit compatibility rules and a schema registry that tracks versions.
Structured elaboration:
- Why it matters for feature pipelines. A feature pipeline has many producers (upstream services or teams writing events) and many consumers (transformation jobs, models, other teams' pipelines); if a schema change breaks any consumer, the failure can be silent (a consumer misinterprets a field) or loud (a consumer crashes), and both are costly to debug after the fact, so having explicit rules for what changes are safe is far cheaper than discovering the hard way.
- How Avro, Parquet, and Protobuf support it. Avro attaches the writer's schema to the data (or references it via a schema registry) and defines resolution rules for reading data written with an older or newer schema than the reader expects; adding a field with a default value is backward compatible, removing a field a reader depends on is not. Parquet stores its schema in the file's footer and supports schema merging across files with compatible-but-not-identical schemas (commonly used when a table's schema evolves over time across many files), though it relies more on the query engine to reconcile differences than Avro's explicit reader/writer resolution. Protobuf uses numbered fields, where adding a new numbered field is safe (old code ignores it, new code sees it as absent/default in old data) and reusing or renumbering an existing field number is unsafe (it silently reinterprets old data incorrectly).
- Compatibility models. Backward compatibility means new code can read data written by old code (safe: adding an optional field); forward compatibility means old code can read data written by new code (safe: old code ignoring a new field it doesn't know about); full compatibility requires both directions to hold simultaneously, which is the strictest and safest guarantee for a shared schema used by many independent consumers who upgrade on different schedules.
- Handling a breaking change with multiple consumers. For a genuinely breaking change (renaming a field, changing its type incompatibly), the standard process is: introduce the new field alongside the old one (dual-write), migrate consumers to the new field on their own schedule, monitor until no consumer is still reading the old field, then remove the old field in a later, separate release, rather than attempting an atomic cutover across every consumer simultaneously.
Worked example: Adding a new optional device_type field to an event schema with a default value of "unknown" is backward compatible under all three formats: existing consumers that do not know about the field simply ignore it, and consumers upgraded to read it get "unknown" for historical data that predates the field's introduction, with no reprocessing required.
Trade-offs & pitfalls: A schema registry enforcing compatibility checks at write time (rejecting a producer's schema change that would break existing consumers) is the strongest guardrail, but it requires the discipline of registering every schema change through the registry rather than a producer silently writing a new shape; a team that bypasses the registry (writing raw JSON with no enforced schema, for example) loses this protection entirely and reintroduces the risk the registry exists to prevent. The dual-write-then-migrate-then-remove pattern for breaking changes is the safe default, but it is slower than a direct cutover, and teams under time pressure sometimes skip the migration step and go straight to removing the old field, which breaks any consumer that had not yet migrated, often silently.
Case study: a production model's accuracy dropped after a feature-store ingestion pipeline was modified. Walk through the incident response: immediate mitigation and rollback options, how you would reproduce the issue and find the root cause, what you would validate before confirming a fix, and the long-term changes you would make to prevent recurrence.
Sample Answer
Direct answer: The incident-response walkthrough has four phases: stop the bleeding (immediate mitigation or rollback), find the root cause (reproduce and isolate what the pipeline change actually did differently), confirm the fix is genuinely correct (not just "the numbers look better now"), and prevent recurrence (a structural change, not just a one-time patch).
Structured elaboration:
- Immediate mitigation. If the pipeline change is recent and identifiable, the fastest safe action is usually to roll back the ingestion-pipeline change to its last-known-good state, restoring the feature computation to what it was before accuracy dropped, while investigation continues in parallel; if a clean rollback is not straightforward (the change is entangled with other work, or has already run for a while and reverting risks its own inconsistency), a feature-flag-style disable or fallback to a simpler, previously-validated feature version can serve as a faster, lower-risk mitigation.
- Reproduction and root cause. Compare the feature values computed by the old pipeline logic against the new, for the same time range and same entities, to isolate exactly what changed (a specific field's values shifted, a join started dropping rows, a null-handling change altered a downstream calculation); this data-diff approach (the same technique used in the CI/CD sub-area of this topic) turns "something about the pipeline change broke it" into a specific, falsifiable hypothesis about which change caused which effect.
- Validation before confirming the fix. Once a root cause is identified and a fix is written, validate it the same way the original bug was found: rerun the data diff between the fixed pipeline and the known-good baseline, confirming the fix produces matching (or intentionally, explainably different) output, and, where feasible, validate the fix's downstream effect on model accuracy in a staging/shadow environment before re-deploying to production, rather than assuming the fix is correct just because the code change looks reasonable.
- Long-term prevention. Depending on what the root cause turns out to be, the structural fix might be: adding a data-diff check as a mandatory CI gate for pipeline changes (so this class of bug is caught pre-deployment next time), adding a monitoring alert on the specific signal that would have caught this sooner (a sudden shift in a feature's null rate or distribution), or a canary/shadow-deployment requirement for feature-pipeline changes going forward.
- Cross-team coordination. Throughout, keep the model-owning team informed (they need to know whether to trust recent predictions, and whether a retraining or rollback of the model itself is warranted separately from the pipeline fix) and document the incident (what happened, root cause, fix, and the prevention measure) for the postmortem process, which is a distinct exercise from this technical walkthrough.
Worked example: A recent change to a feature-store ingestion job that was meant to add a new field for a different purpose accidentally altered a join condition, causing an existing feature to silently drop rows for entities with a specific upstream attribute; a data diff between the old and new pipeline output over the last 48 hours pinpoints exactly which entities and which feature are affected, the rollback restores correct values within the hour, the fix (correcting the join condition, verified via the same data-diff technique) deploys after a data-diff-clean and shadow-validated check, and the long-term prevention is adding an automated row-count and null-rate sanity check to the pipeline's CI gate so a join regression like this is caught before deployment in the future.
Trade-offs & pitfalls: Rolling back quickly without first understanding what changed can occasionally roll back a legitimate, needed fix along with the regression if the pipeline change bundled multiple unrelated modifications together, which is itself a lesson about keeping pipeline changes small and independently deployable. Confirming a fix by only checking that "accuracy looks better now" without doing the data-diff validation risks declaring victory on a fix that happens to look better due to unrelated factors (natural variance, a different time period's data characteristics) rather than because it actually addressed the root cause, which can let the real bug persist and resurface later.
A model has started showing more false positives, and you suspect a mismatch between offline feature computation and online serving retrieval. Describe a plan to detect, reproduce, and fix issues caused by inconsistent feature computation (for example, a stale cache, missing keys, or a serialization difference), including the instrumentation and tests you would add to prevent recurrence.
Sample Answer
Direct answer: Detecting and reproducing a training-serving inconsistency starts by comparing the exact feature values the model saw at training time against what the online path currently returns for the same entities, narrowing down whether the divergence is a stale cache, a missing key, or a serialization mismatch, and closing the loop with instrumentation that would catch this class of bug automatically going forward.
Structured elaboration:
- Detect. Set up a systematic comparison: for a sample of entities, pull their feature values from the offline (training-time) computation and from the online serving path for the same point in time, and diff them; a consistently non-zero diff rate (not just occasional noise) confirms a real inconsistency rather than expected minor timing differences.
- Reproduce. Narrow to specific entities showing the largest divergence and trace each one's value through the pipeline: what did the offline computation produce, what does the online store currently hold, and when was the online value last updated, which starts distinguishing the candidate causes.
- Stale cache. If the online value's last-updated timestamp is old relative to when it should have refreshed, the online materialization pipeline itself may be lagging or failing silently for a subset of entities (worth checking if it correlates with a specific partition or shard); the fix here is addressing the materialization lag/failure, not the feature logic itself.
- Missing keys. If some entities have no online value at all (falling back to a default or null that the offline path would not have produced), trace whether those entities are newly created (a cold-start gap between when an entity is created and when it first gets materialized online) or whether a upstream join or filter is silently excluding them from the online materialization pipeline that the offline pipeline does not exclude them from.
- Serialization differences. If values exist on both sides but differ, check whether the offline and online computation paths are using genuinely the same transformation logic (a common root cause is the two paths having independently-implemented, subtly different logic, e.g. different rounding, different null-handling, or a unit mismatch) versus a serialization/deserialization bug (a type coercion, an encoding mismatch) that corrupts an otherwise-correct value on the way to or from the online store.
- Instrumentation and tests to prevent recurrence. Add an automated, continuously-running online/offline consistency check (not just a one-time investigation) that samples entities and alerts if the divergence rate exceeds a threshold; add a unit or integration test asserting the offline and online computation paths, when fed the same input, produce identical output, so a future code change that lets the two paths drift apart is caught in CI rather than in production months later.
Worked example: The investigation finds that entities with a specific upstream data source (say, a newer mobile client version) show the highest divergence; tracing one such entity reveals the online path's transformation logic was updated recently to handle a new field from that client version, but the corresponding offline (batch) transformation logic was never updated to match, meaning the two paths have silently diverged for exactly the population using the new client, which explains both the false-positive symptom (features computed differently for a subset of the population) and why it was not caught immediately (it only affected a growing but still partial slice of traffic).
Trade-offs & pitfalls: A common trap is fixing the specific instance of divergence found (patching the online or offline logic to match) without addressing why the two paths were able to drift apart in the first place; the durable fix is usually architectural, such as sharing the actual transformation code between the offline and online paths (a single source of truth for the logic, executed in both a batch and a streaming/serving context) rather than maintaining two independently-written implementations that have to be manually kept in sync, which is the root cause this class of bug keeps recurring from in practice.
A website's click events arrive at 50,000 events per second. Analytics needs hourly aggregates, but an online recommender needs features fresher than 5 seconds. Compare batch and streaming ingestion architectures for this workload, discussing trade-offs in latency, cost, operational complexity, state management, and fault tolerance, and give a recommendation.
Sample Answer
Direct answer: At 50,000 click events per second with two very different downstream needs (hourly aggregates for analytics, sub-5-second freshness for a recommender), the right design runs both a streaming path and a batch path off the same ingested event log, rather than forcing one architecture to serve both needs.
Structured elaboration:
- Ingestion. All 50,000 events/sec land in a durable log (Kafka), decoupling ingestion rate from either downstream consumer's processing rate and giving replay capability for both paths.
- Streaming path (for the recommender). A stream processor consumes the log continuously, computing the near-real-time features (e.g. a short rolling window of recent activity) and writing them to a low-latency online store, meeting the sub-5-second freshness requirement.
- Batch path (for hourly analytics). The same log (or a copy of the raw events landed in object storage) is processed hourly by a batch job that computes the coarser aggregates, writing to the offline/analytics store; this path can be simpler and cheaper since it does not need to meet a tight latency bound.
- State management. The streaming path needs bounded, checkpointed state for its rolling windows (as covered in the windowing/watermarking sub-area of this topic); the batch path largely avoids persistent state since each run processes a complete, bounded chunk of data.
- Fault tolerance. The streaming path needs checkpointing and a fast-recovery story (a few seconds of downtime is tolerable, a few minutes may violate the freshness SLA (service-level agreement)); the batch path's fault tolerance is simpler (a failed run is retried from the last successful checkpoint or rerun entirely for that hour).
Worked example: At 50,000 events/sec, an hourly batch job processes roughly 180 million events per run; a well-provisioned Spark job on a modestly sized cluster comfortably aggregates that volume within a small fraction of the hour it has available, leaving generous headroom for retries. The streaming path processing the same 50,000 events/sec continuously needs enough parallelism (partitioned consumers) that no single task falls behind the source rate, which is a much tighter, always-on capacity requirement than the batch path's "finish comfortably within an hour" bar.
Trade-offs & pitfalls: Running both a streaming and a batch path off the same source (a lambda-architecture-style split) means the two paths' feature-computation logic can drift apart over time if not carefully shared or tested against each other, producing subtly different numbers for what is conceptually "the same" underlying metric at different time granularities; a common mitigation is sharing the core transformation logic between both paths where possible, and monitoring for large divergence between the streaming path's rolled-up hourly total and the batch path's independently-computed hourly total as a correctness check.
Scenario: Your streaming engine's state store is growing without bound and causing OOM and long GC pauses. Describe immediate mitigations and long-term strategies: state compaction, TTL eviction, checkpointing, incremental snapshots, windowing changes, and architectural alternatives (e.g., external state store).
Sample Answer
Direct answer
Immediate mitigations buy time without a code change: shorten the watermark threshold (drops old, rarely-needed state faster) and, if the streaming query supports it, force an early checkpoint/restart with a tighter configuration to release currently-held state. Long-term, the real fix is almost always adding or shortening a TTL (time-to-live) on state that should not live forever, since unbounded state growth is usually a symptom of state accumulating for keys that will never see another matching event, not a sizing problem alone; if the state genuinely needs to outlive what Spark's in-memory/RocksDB state store can hold efficiently, moving to an EXTERNAL state store (or redesigning the windowing to not need unbounded per-key history at all) is the architectural escape valve.
Structured elaboration
Why state grows unbounded in the first place. Spark's Structured Streaming state store holds accumulating per-key state for stateful operations (a windowed aggregation's running totals, a flatMapGroupsWithState custom aggregation's accumulated values, a stream-stream join's buffered unmatched rows) across micro-batches; WITHOUT a watermark (or with one set too generously wide), state for a key is never considered "safe to drop," so keys that appear once and never again still hold their state entry FOREVER, and the state store's total footprint grows monotonically with the number of DISTINCT keys ever seen, not the number of ACTIVE keys at any given time.
Immediate mitigations.
- Shorten the watermark threshold. A watermark that is wider than the REAL lateness distribution needs (sizing it against real data) holds state open longer than necessary; tightening it to match the ACTUAL observed lateness (not an arbitrarily generous default) directly bounds how long a window's state stays open before being released.
- State compaction / a forced checkpoint cycle. RocksDB-backed state stores (the default for larger stateful workloads) accumulate internal fragmentation over many micro-batches; triggering compaction (
spark.sql.streaming.stateStore.rocksdb.compactOnCommitor a similar maintenance operation, depending on Spark version) reclaims space from already-tombstoned (deleted-but-not-yet-reclaimed) state entries without needing a full restart.
Long-term strategies.
- TTL eviction. For stateful operations expressed via
flatMapGroupsWithState/applyInPandasWithState(arbitrary custom stateful logic, this API), explicitly setting a TIMEOUT (event-time or processing-time based) on each group's state ensures a key with no new events for the configured duration has its state EXPLICITLY cleared, rather than relying solely on watermark-driven window closure (which only applies to the built-in windowed-aggregation shape, not arbitrary custom state). - Incremental snapshots. Rather than a full state-store snapshot on every checkpoint (expensive and slow for very large state), Spark's state store supports INCREMENTAL checkpointing (only the DELTA since the last snapshot), reducing both the per-checkpoint cost and the RECOVERY-time cost of replaying from a checkpoint, a meaningful operational win once state size grows large enough that full snapshots themselves become a bottleneck.
- Windowing changes. If the query's CURRENT design needs per-key state to live indefinitely because the business logic is framed as "track this forever," reframing to a BOUNDED window (a rolling 30-day window instead of "all time," for instance) if the business requirement genuinely tolerates it directly bounds state size by construction, the most durable fix when it is available.
- External state store. If the required state genuinely cannot be bounded by any of the above (a business requirement to track something truly indefinitely, at a scale beyond what an in-cluster state store can hold efficiently), moving that state to an EXTERNAL, purpose-built store (a key-value store queried by the stateful operation instead of held in Spark's own state store) is the architectural escape valve, at the cost of added system complexity (network round-trips to the external store per micro-batch, a new dependency to operate) that should be reached for only once the simpler, in-Spark options are genuinely insufficient.
Worked example
A Structured Streaming job tracking "days since a user's last purchase" via flatMapGroupsWithState, keyed by user_id, with NO timeout configured: every user who EVER made a purchase, even years ago and never again, still holds an open state entry, and the state store's total size grows with the CUMULATIVE user count over the query's entire lifetime, not the count of users who are still ACTIVE.
Immediate mitigation: if a watermark-based windowed aggregation were ALSO present in the same query, tightening its threshold helps that PART of the state, but does nothing for the flatMapGroupsWithState state specifically, since watermark-driven closure only applies to the built-in windowed-aggregation shape.
Long-term fix: add an explicit event-time timeout to the flatMapGroupsWithState call (e.g., GroupStateTimeout.EventTimeTimeout with a 180-day threshold): a user with no purchase activity for 180 days has their state entry explicitly cleared on the next batch that crosses that threshold, bounding the state store's size to roughly "distinct users active within the last 180 days," not "every user ever seen," directly addressing the actual root cause (a missing TTL on genuinely stateful, indefinitely-accumulating tracking) rather than just buying time with tighter checkpointing.
Trade-offs and pitfalls
- Common mistake: tightening the watermark and declaring the problem solved, when the actual unbounded growth is coming from a
flatMapGroupsWithStateoperation the watermark does not govern at all; diagnosing WHICH stateful operation is actually accumulating (via the Spark UI's Structured Streaming state-store metrics, which report size PER operator) before choosing a fix avoids applying the wrong lever. - Common mistake: reaching for an external state store as the first response to state growth, when a properly-configured TTL/timeout on the existing state operations would have solved it with far less added complexity; treat the external store as the LAST option once the simpler, in-Spark fixes are confirmed insufficient, not the first instinct.
- A TTL that is too aggressive drops state for a genuinely still-relevant key too early, a correctness cost (losing legitimate history) traded for a memory savings; size the TTL against the REAL business meaning of "this key is no longer relevant," not purely against memory pressure.
- Incremental snapshots reduce checkpoint COST but do not by themselves bound state SIZE; they help the operational overhead of an already-large state store, but do not address the underlying unbounded-growth root cause the way a TTL/timeout does, worth applying alongside a real bounding fix, not as a substitute for one.
Unlock Full Question Bank
Get access to all 20 ML Feature Pipelines and Feature Stores interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.