← Back to all questions
StaffAIFeature Store

Design an ML Feature Store for Personalization at Scale

A Staff-level walkthrough of a unified ML feature platform serving 200M+ users — one feature definition materialized into both a batch (Spark) and streaming (Flink) pipeline, an online store tuned for <5ms p99 and an offline store that produces point-in-time-correct training sets. The recurring tension is training-serving consistency: identical logic must yield identical values online and offline, with no future signal leaking into training. Follow it top to bottom, or jump to any step.

Level
Staff
Category
ML Infrastructure · Feature Store
Interview time
60 min
100% free · No login required
WHAT THIS QUESTION TESTS
·One feature definition, dual-materialized to batch and streaming
·Online store (EVCache-style) engineered for <5ms p99 reads
·Offline point-in-time joins for leakage-free training data
·Registry plus catalog so teams discover and reuse features
★ STAFF-LEVEL SIGNALS
Budgets the serialization choice (Protobuf vs JSON/FlatBuffers) against the <5ms latency ceiling
Treats point-in-time correctness as a model-quality risk — leakage inflates offline AUC, then dies in A/B
Monitors distribution drift and training-serving skew with PSI/KL thresholds, not just freshness
Frames 60%+ feature reuse as the ROI that justifies the platform's existence
0

Scope & ambiguity

“Netflix’s personalization stack runs 100+ ML models across recommendations, search ranking, artwork personalization, and content valuation. The key challenge is consistent feature computation between training and serving and low-latency online serving for real-time recommendations. I’d scope this as a platform providing a unified feature registry, dual-write pipelines (batch + streaming), and online/offline serving — so any ML team defines features once and uses them reliably across training and inference.”

Phases:

Phase
Timeline
Scope
Phase 1
0-3 months
Core feature registry + online store (EVCache) + offline store (S3/Parquet) + batch ingestion (Spark)
Phase 2
3-9 months
Streaming features (Flink) + point-in-time joins + feature catalog/discovery UI + monitoring (Atlas)
Phase 3
9-18 months
Automated feature engineering + on-device computation (privacy-preserving) + backfill automation
1

Requirements

Functional

  • FR1: Register features — define name, entity type, computation logic, freshness SLA, owner team
  • FR2: Dual compute — execute feature logic as both batch (Spark) and streaming (Flink) from a single definition
  • FR3: Online serving — serve feature vectors with <5ms p99 latency via EVCache
  • FR4: Offline point-in-time — generate training datasets with features joined as-of each example's timestamp
  • FR5: Feature catalog/discovery — searchable registry so teams find and reuse existing features
  • FR6: Quality monitoring — track freshness, null rates, distribution drift, training-serving skew via Atlas

Non-Functional

Requirement
Target
Online serving latency
p50 < 1ms, p99 < 5ms
Batch feature freshness
< 1 hour
Streaming feature freshness
< 30 seconds
Training-serving skew
< 0.1% divergence
Feature registry availability
99.99%
Online store availability
99.99%
Scale
200M+ users, 10K+ features, 100+ models
2

Back-of-envelope estimation

Metric
Value
Active users
230M
Titles in catalog
17K
Features per user
~200 (batch + streaming)
Online read QPS
2M reads/sec (peak, across all models)
Streaming events ingested
500K events/sec (viewing, browsing, search)

Storage

Store
Calculation
Size
EVCache (online)
230M users x 200 features x ~1KB serialized
~460 GB
S3/Parquet (offline)
230M users x 200 features x 30 days
~138 TB
Cassandra (registry)
10K features x ~5KB each
~50 MB (negligible)

Estimated cost: ~$450K/month (EVCache ~$200K, S3 ~$100K, Spark/Flink compute ~$150K).

Key insight: EVCache is the dominant cost and latency-critical path. Compression, TTL tuning, and tiered storage are the biggest levers.

3-4

API & data model

Feature Registry REST API

POST /api/v1/features
{
"name": "user_genre_affinity",
"entity_type": "user",
"value_type": "FLOAT_VECTOR",
"dimensions": 128,
"computation": {
"batch_sql": "SELECT user_id, weighted_avg(genre_vec, watch_dur) ...",
"streaming_logic": "sliding_window_avg(genre_vec, 30d)",
"freshness_sla": "1h"
},
"owner_team": "personalization-algorithms"
}
 
GET /api/v1/features/{feature_name}
PUT /api/v1/features/{feature_name} // versioned, backward-compatible

Feature Serving gRPC API

service FeatureServing {
rpc GetOnlineFeatures(OnlineFeatureRequest) returns (OnlineFeatureResponse); // p99 < 5ms
rpc GetTrainingFeatures(TrainingFeatureRequest) returns (stream TrainingFeatureResponse);
}
 
message OnlineFeatureRequest {
repeated string entity_ids = 1; // ["user:12345", "title:tt0903747"]
repeated string feature_names = 2; // ["user_genre_affinity", "title_popularity"]
}
 
message TrainingFeatureRequest {
repeated string entity_ids = 1;
repeated string feature_names = 2;
repeated int64 point_in_time_timestamps = 3; // prevents data leakage
}

Kafka topic: features.computed — emitted after each batch/streaming write with feature name, entity ID, version, timestamp, and checksum.

Data Entities

Entity
Storage
Key Format
Rationale
FeatureDefinition
Cassandra
feature:{name}:v{version}
Flexible schema, Netflix standard
OnlineFeatureValue
EVCache
features:{entity_type}:{entity_id}:v{version}
Sub-ms reads, AZ-replicated
OfflineFeatureValue
S3/Parquet
s3://features/{entity_type}/date={YYYY-MM-DD}/
Columnar, partition-pruned joins
FeatureLineage
Kafka + S3
Event stream archived to S3
Audit trail, debugging
FeatureMetrics
Atlas
atlas.features.{name}.{metric}
Netflix telemetry standard
5

High-level architecture

[Data Sources] [Consumers]
Viewing history ──┐ ┌── Model Training (Spark ML, TF)
Search queries ──┤ +------------------+ │
Browse behavior ──┼──>| Feature Compute |─────────┤
Ratings ──┤ | Engine | └── Model Serving (Rec API, Search)
Content metadata──┘ +--+------------+--+
| |
[Batch Pipeline] [Streaming Pipeline]
(Spark, hourly) (Flink, real-time)
| |
+-----v----+ +-----v----+
| Offline | | Online | +------------------+
| S3/Parquet| | EVCache |<--->| Feature Serving |
+-----+----+ +----------+ | API (gRPC <5ms) |
| +------------------+
[Training Data Gen]
[Point-in-Time Joins]
 
+------------------+ +------------------+ +------------------+
| Feature Registry | | Feature Catalog | | Feature Monitor |
| (Cassandra) | | (Discovery UI) | | (Atlas + Alerts) |
+------------------+ +------------------+ +------------------+

Service Boundaries

Service
Team
Data Store
SLA
Feature Registry
Feature Platform
Cassandra
99.99% availability
Feature Serving API
Feature Platform
EVCache (read)
p99 < 5ms, 99.99%
Batch Pipeline
Feature Platform
S3 + EVCache (write)
Freshness < 1hr
Streaming Pipeline
Feature Platform
EVCache (write)
Freshness < 30s
Training Data Generator
ML Platform
S3 (read)
Correctness > 99.9%
Feature Monitor
Observability
Atlas
Alert latency < 2min

Failure Domains

ID
Failure
Mitigation
FD1
EVCache cluster failure
AZ-replicated, fallback to stale cache + batch defaults
FD2
Spark batch pipeline failure
Alert on freshness SLA, auto-backfill from checkpoint
FD3
Flink streaming pipeline failure
Fall back to batch values, Flink savepoints for recovery
FD4
Cassandra registry down
SDK caches definitions locally, serving unaffected
6

Deep dives

WHERE STAFF IS WON

Architecture (Netflix-Specific)

Netflix stack: AWS-native, Spark for batch, Flink for streaming
 
[Data Sources: viewing history, search queries, browse behavior, ratings]
|
[Feature Pipelines]
Batch (Spark, hourly/daily):
- user_avg_watch_duration_30d
- user_genre_affinity_vector (128-dim)
- title_popularity_by_country
Streaming (Flink, real-time):
- user_last_5_titles_watched
- user_current_session_duration
- title_views_last_1h (trending signal)
|
+---+---+
| |
[Offline Store] [Online Store]
(S3 + Parquet) (EVCache/Memcached)
| |
[Model Training] [Model Serving]
(Spark ML, (Recommendation API,
TensorFlow) ranking, search)

EVCache (Netflix’s Caching Layer)

Netflix's fork of Memcached, purpose-built for their use case:
- Replicated across AZs (data not lost on single AZ failure)
- Thousands of instances, petabyte-scale
- Sub-millisecond reads for feature vectors
 
Feature serving:
Key: "features:user:{user_id}:v3"
Value: serialized feature vector (Protobuf, ~2KB per user)
TTL: 1 hour (refreshed by streaming pipeline)
 
Batch: overwrite features daily (Spark job → EVCache bulk load)
Streaming: update specific features in real-time (Flink → EVCache put)

Training-Serving Consistency

Single feature definition → dual materialization (same as Meta's approach):
 
Feature definition:
name: user_genre_affinity
entity: user_id
computation: weighted_avg(genre_vector, watch_duration) over last 30 days
online_freshness: 1 hour
offline_freshness: 1 day
 
Training data construction:
- Point-in-time join: for each training example at time T, use features as of time T
- Prevents data leakage (no future features in training data)
- Implemented via timestamp-versioned offline store (Parquet partitioned by date)

Feature Serialization Format

I’d choose Protobuf for feature vector serialization:

Format
Size (200 features)
Serialize
Deserialize
Schema Evolution
Choice
JSON
~4.5 KB
~0.8ms
~1.2ms
Flexible, no type safety
No — too large/slow
Protobuf
~1.8 KB
~0.1ms
~0.15ms
Backward/forward compat
Yes
FlatBuffers
~2.0 KB
~0.05ms
~0.02ms
Forward only
No — rigid evolution
MessagePack
~2.2 KB
~0.3ms
~0.4ms
Schema-less
No — no type safety

Protobuf wins: ~60% smaller than JSON, fast encode/decode within <5ms budget, robust schema evolution for 100+ independently-evolving model consumers.

message FeatureVector {
string entity_id = 1;
int64 computed_at = 2;
int32 feature_version = 3;
map<string, FeatureValue> features = 4;
}
 
message FeatureValue {
oneof value {
double float_val = 1;
int64 int_val = 2;
string string_val = 3;
FloatArray vector_val = 4; // embedding features
StringArray list_val = 5; // categorical lists
}
}

Point-in-Time Correctness

The problem: Data leakage silently kills model quality. Features computed after the label event create signals that won’t exist at inference time — +5% offline AUC that vanishes in production A/B tests.

For training example: (user_123, title_456, clicked=True, timestamp=T)
 
WRONG: SELECT features WHERE user_id = 'user_123' → latest, includes future
RIGHT: SELECT features WHERE user_id = 'user_123' AND computed_at <= T → as-of time T

Implementation: Parquet partitioned by entity_type/date/ with event_timestamp column. Training data generator performs asof-join: for each example at time T, read latest partition where date <= T, filter event_timestamp <= T.

Approach
Storage Cost
Query Speed
Correctness
My Choice
Daily snapshots
High (full copy/day)
Fast (partition pruning)
Day-level
Yes — sufficient for batch
Event-sourced log
Low (deltas)
Slow (replay)
Exact
Phase 2 for streaming features
Bi-temporal
Medium
Medium
Exact
Future consideration

Feature Computation Pipelines

Batch Pipeline (Spark):

Schedule: hourly for critical features, daily for stable ones
 
1. Spark reads from data warehouse (viewing events, user profiles, title metadata)
2. Executes feature computation DSL from registry
3. Writes Parquet to S3 (offline store) — partitioned by entity_type/date
4. Triggers EVCache bulk load — Spark partitions → parallel PUT to EVCache
5. Publishes completion event to Kafka (features.computed topic)
6. Validates output: row count, null rate, distribution stats → Atlas
 
Optimization: incremental compute — only reprocess users with new events
since last run (reduces Spark job from 45min → 8min)

Streaming Pipeline (Flink):

1. Flink consumes from Kafka topics (viewing, search, browse events)
2. Applies sliding window aggregations:
- user_last_5_titles: tumbling window, append-on-event
- title_views_last_1h: sliding 1h window, count aggregate
- user_session_duration: session window with 30min gap
3. Writes directly to EVCache (online store only)
4. Publishes update events to Kafka for lineage tracking

Backfill: Dedicated lower-priority Spark job recomputes across full historical window when features are registered or computation bugs are fixed. Writes to S3 and bulk-loads into EVCache.

Failure handling:

  • Batch fails → online store serves last-good values (TTL extended), auto-retry 3x, then page on-call
  • Streaming fails → Flink restarts from savepoint (exactly-once), events buffered in Kafka (7-day retention) for replay

Feature Registry and Catalog

Registry (Cassandra): Versioned feature definitions tracking name, entity type, computation logic, freshness SLA, owner team, upstream sources (lineage), downstream consumers, and quality metrics.

Catalog UI: Searchable by name/entity/tag, shows distribution stats and lineage (data source → feature → models), supports access requests across teams.

Deprecation: 14-day warning to consumers → read-only mode → computation stops → S3 data retained 90 days → archived.

Feature reuse target: 60%+ — at least 60% of features in any new model from the shared catalog. This is the primary ROI justification; without reuse it’s just an expensive pipeline.

Feature Monitoring

Drift detection: Compute PSI and KL-divergence between training-time and live serving distributions. Alert at PSI > 0.25 (significant shift).

Example: user_genre_affinity vector
Training (Jan): mean=0.45, std=0.12, null_rate=0.02%
Serving (March): mean=0.52, std=0.18, null_rate=0.8%
PSI = 0.31 → ALERT: content catalog shift or upstream bug

Freshness (Atlas): Track last_computed_at per feature, alert when staleness exceeds SLA. Dashboard across all 10K features sorted by staleness.

Null rate: Alert if > 5% for any feature (upstream data issue). Track coverage: % of active users with feature computed.

Training-serving skew: Periodically sample 1000 serving requests, re-compute offline, compare. Alert if divergence > 1% (consistency bug in dual materialization).

7

Multi-Team Rollout

Phase 1: Foundation (Weeks 1-8)

  • Build registry service (Cassandra) + Python/Java SDK
  • Deploy EVCache cluster + gRPC serving API (<5ms p99)
  • Pilot 1 team (Recommendations) — 20 features in shadow mode
  • Success: feature values match existing pipelines within 0.1%

Phase 2: Batch Pipeline (Weeks 9-16)

  • Spark batch pipeline + DSL compiler (definition → Spark SQL)
  • Migrate pilot team from ad-hoc jobs to feature store
  • Validate point-in-time join engine, launch catalog UI
  • Success: pilot fully migrated, training data quality validated

Phase 3: Streaming Pipeline (Weeks 17-24)

  • Flink streaming pipeline for real-time features
  • Deploy monitoring dashboard (Atlas: freshness, drift, null rates)
  • Onboard 2nd team (Search Ranking) — 30 features
  • Success: streaming <30s freshness, 2 teams onboarded

Phase 4: Platform Rollout (Months 7-12)

  • Self-service onboarding, feature sharing (catalog read-write)
  • Migration tool (auto-converts ad-hoc Spark jobs → feature store definitions)
  • Target: 80% models on feature store, 60%+ reuse rate

Rollback Plan

Phase
Rollback Strategy
Phase 1-2
Revert to ad-hoc pipelines — still running in parallel during shadow mode
Phase 3
Batch-only fallback — streaming features degrade to hourly freshness
Phase 4
Fix forward — canary deployments and feature flags per team
8

Bottlenecks & evolution

Bottlenecks

Bottleneck
Impact
Mitigation
EVCache memory pressure
OOM risk as features grow
Protobuf compression (~40% savings), tiered storage (hot in EVCache, cold in S3)
Spark batch job duration
Freshness SLA misses
Incremental compute (changed entities only), partitioned jobs
Flink backpressure
Streaming staleness at traffic spikes
Auto-scale task managers, Kafka buffering (7-day retention)
Registry SPOF
Pipeline failures
Cache definitions in SDK (5min TTL), Cassandra handles read-heavy
Training data joins
Slow dataset gen blocks model iteration
Pre-materialized datasets for common entity/time ranges

Observability (RED Metrics)

Service
Rate
Errors
Duration
Feature Serving API
2M req/sec
< 0.01%
p50 < 1ms, p99 < 5ms
Batch Pipeline
24 runs/day
< 2% failure
< 45min/run
Streaming Pipeline
500K events/sec
< 0.001% dropped
< 30s e2e
Registry API
~100 req/sec
< 0.1%
p99 < 50ms

Critical Alerts

Alert
Threshold
Severity
Action
Feature freshness SLA breach
now() - last_computed > SLA
P1
Page on-call, investigate pipeline
Training-serving skew
Divergence > 1% any feature
P1
Halt training, investigate dual materialization
EVCache hit rate drop
Hit rate < 99%
P2
Check TTL, investigate eviction pressure
Feature null rate spike
Null rate > 5% any feature
P2
Investigate upstream data source

Evolution

Year 1: Foundation — core feature store with online/offline stores, batch + streaming pipelines, monitoring. Target 80% of ML models migrated from ad-hoc pipelines.

Year 2: Feature sharing catalog with cross-team discovery, automated drift detection (PSI/KL-divergence alerting), GPU-accelerated feature computation for embeddings (recomputation from 45min → 5min on GPU cluster).

Year 3: On-device feature computation for privacy-preserving personalization, AutoML integration (auto-generate candidate features from raw data), federated feature store across global regions, target 30% infrastructure cost reduction via intelligent caching and tiered storage.

Rubric — Senior vs Staff

Dimension
Senior signal
Staff signal
Training-serving consistency
Keeps features in both an online and an offline store.
Single definition, dual materialization; samples live traffic to detect skew and halts training on >1% divergence.
Point-in-time correctness
Mentions joining features by timestamp for training.
Explains leakage as inflated offline AUC; implements asof-joins over date-partitioned Parquet with computed_at <= T.
Online serving
Caches feature vectors for low-latency reads.
Defends the <5ms p99 budget explicitly — Protobuf for size/speed, AZ-replicated cache with stale-value fallback for availability.
Compute pipelines
Runs separate batch and streaming jobs.
Incremental compute, savepoint/replay recovery, Kafka-buffered backfill, and per-feature freshness SLAs.
Platform adoption
Builds a registry of feature definitions.
Drives reuse (60%+ target), shadow-mode rollout, self-service onboarding, and a deprecation lifecycle as the ROI story.
★ MORE WALKTHROUGHS

Want more breakdowns like this?

Join free early access for upcoming RAG, LLM eval, agents, and AI infrastructure walkthroughs.

Join Free Early Access →