Design a recommendation system
The interviewer asks
“Design a recommendation system for a large consumer app, for example "Recommended for you" on an e-commerce home page or the next videos to watch. It should personalise for each of ~100M users and react to what they do in the current session.”
What a strong answer looks like
A strong answer frames recommendations as a multi-stage funnel and separates offline training from online serving. Candidate generation pulls a few thousand items from several sources (two-tower embedding retrieval with an ANN index, item-to-item co-occurrence, trending, recently viewed). A ranking model (gradient-boosted trees or a deep model) scores a few hundred candidates with user, item and context features read from a feature store. A re-ranking layer applies business rules: diversity, freshness, dedupe, already-purchased filtering, sponsored slots. Events stream through Kafka into both real-time features (last items viewed) and the data lake for daily training. The candidate must talk about cold start, feedback loops and popularity bias, training-serving skew, and evaluation with offline metrics plus online A/B tests on the real business metric.
Concepts to mention
1 Requirements
Functional
- Return a ranked list of N items (products, videos) for a user on a given surface (home, product page, cart)
- Personalise using long-term history (purchases, watches) and the current session (last few clicks)
- "Similar items" and "bought together" for a given item
- Handle new users and new items (cold start)
- Respect business rules: no out-of-stock items, no already-bought durable items, sponsored slots, content policy
- Log impressions and interactions for training and measurement
Non-functional
- p99 serving latency under 150 ms for the full pipeline
- ~50k recommendation requests/s at peak
- Session signals affect the next request within ~1-5 seconds
- New items recommendable within ~1 hour of listing
- Availability 99.9%; on failure fall back to cached or popular items, never an empty widget
- Models retrained at least daily; experiments run on 1-5% traffic slices
Out of scope (say it out loud)
- Search ranking (separate system, though it may share features)
- The details of model architecture research
- Ad auction and billing
- Notification and email recommendation scheduling
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
GET/v1/recommendations?surface=home&user_id={id}&context_item={item_id}&n=20 | Ranked items for a surface; response includes a request_id used to join impressions and clicks |
GET/v1/items/{item_id}/similar?n=20 | Item-to-item recommendations (similar, bought together) |
POST/v1/events | Client logs impressions, clicks, add-to-cart, watch time; batched from the app |
gRPCFeatureStore.Get(entity_ids, feature_names) | Internal: online feature lookup for ranking |
4 Data model
eventsKafka -> data lake (Parquet on S3, partitioned by date/hour)event_idrequest_iduser_iditem_idsurfacepositionevent_typetscontext (device, city)Immutable log. request_id joins what was shown with what was done, which is essential for unbiased labels.
online_featuresRedis / Aerospike / DynamoDB (feature store online layer)key = user:{id} or item:{id} -> {feature_name: valueupdated_at}Low-latency reads (<5 ms for a batch). Batch features written daily; streaming features (last 20 viewed items, session category counts) written by Flink within seconds.
offline_featuresData lake / warehouse (Hive, BigQuery, Snowflake)same feature definitions with point-in-time timestampsPoint-in-time correct joins prevent label leakage in training data.
item_embeddings_indexANN index (FAISS / ScaNN / Milvus / OpenSearch k-NN)item_idembedding[64]filter attributes (category, in_stock, language)Rebuilt daily from the item tower, with incremental adds for new items. HNSW or IVF-PQ.
item_to_itemKey-value store (Redis / Cassandra)item_id -> [(similar_item_id, score)] top 200Precomputed nightly from co-view / co-purchase counts and embedding similarity.
model_registryS3 + metadata DB (MLflow-like)model_idversionfeatures usedtraining data windowmetricsstatusServing loads models by version; supports canary and rollback.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Serve home-page recommendations
- 1App requests /v1/recommendations?surface=home; the orchestrator loads the user's embedding and recent-session features from the feature store (~5 ms).
- 2Candidate generators run in parallel with a 30 ms budget: two-tower ANN search (~300 items), item-to-item from the last 5 viewed items (~200), trending in the user's city/category (~100), and 'continue where you left off' (~20).
- 3Orchestrator merges and dedupes to ~500 candidates, then applies hard filters: out of stock, already purchased durables, blocked content, wrong language.
- 4Ranking server fetches item features and cross features in batch and scores all candidates with the current model version (~30-40 ms), predicting e.g. p(click) and p(purchase), combined into one score.
- 5Re-ranker takes the top ~100 and applies diversity (max 3 per brand/category in the top 10), freshness boosts, sponsored slots in fixed positions and an exploration slot for new items.
- 6Response returns 20 items with a request_id; the app logs impressions and clicks with that request_id to Kafka.
Learn from a click within seconds
- 1User clicks three running shoes; the app sends click events to /v1/events, which writes them to Kafka.
- 2Flink updates the user's session features: last viewed items, category counts for the last 30 minutes.
- 3Features land in the online store within ~2 seconds.
- 4On the next scroll or page load, item-to-item candidates come from the shoes and the ranker sees the strong session interest, so the widget now shows related shoes and socks.
Daily training and rollout
- 1Spark joins impressions, clicks and orders with point-in-time features from the offline store to build labelled examples.
- 2Two-tower retrieval and ranking models are trained; offline metrics (recall@K for retrieval, AUC/NDCG for ranking) are checked against the current model.
- 3New item embeddings are written and the ANN index is rebuilt; the model is registered.
- 4The new model serves 5% of traffic as an A/B arm; it is promoted only if the business metric (orders per user, watch time) improves without hurting guardrails such as latency and returns.
7 Deep dives
The funnel: why multi-stage
You cannot score 50M items with a heavy model for each request within 150 ms. So the system is a funnel where each stage handles fewer items with more compute per item. Retrieval (candidate generation) must be cheap and high-recall: a two-tower model learns a user embedding and an item embedding whose dot product predicts interaction; item embeddings are precomputed and put into an ANN index (HNSW or IVF-PQ), and at request time you compute the user embedding and fetch the nearest few hundred items in ~5-10 ms. Several retrieval sources are used together because each covers a gap: co-occurrence for 'bought together', trending for freshness, recent views for intent. Ranking is precision-focused: a model with hundreds of features, including cross features like 'user's affinity to this brand' that a two-tower model cannot express. Re-ranking handles list-level goals that a pointwise model ignores: diversity, de-duplication, fairness to sellers, sponsored placements. Say the numbers: 50M -> 500 -> 100 -> 20.
Feature store and training-serving skew
The most common silent bug in recommender systems is that the model sees different feature values in training than in serving. Example: training computes 'clicks in last 7 days' from the warehouse with end-of-day data, while serving reads a streaming counter that includes the last minute; or training accidentally uses data from after the impression (leakage), so offline AUC looks great and online results are flat. A feature store addresses this: features are defined once, computed by the same code for batch and stream, written to an offline store with timestamps and to an online store for serving. Training data is built with point-in-time joins: for an impression at 10:05, use feature values as of 10:05. Another robust practice is to log the exact features used at serving time with the request_id, and train on those logged features. Monitor feature distributions online vs training and alert on drift or missing values.
Cold start and exploration
New users have no history. Use what you do know: signup context (city, language, device, acquisition campaign), onboarding choices, and the first few clicks of the session, which streaming features capture quickly. Until then, show popular items by city and segment. New items have no interactions, so collaborative signals give them nothing. Use content-based embeddings (from title, category, image, price) so the item tower can place a new item near similar ones from day one, and reserve exploration slots: for example one of the top 10 positions goes to new or uncertain items selected by a bandit (Thompson sampling or epsilon-greedy). Without exploration you create a feedback loop: the model only shows items it already likes, collects data only on them and popular items get more popular. Log the propensity (probability an item was shown) so off-policy evaluation and debiasing are possible later.
Measuring success
Offline metrics are necessary but not sufficient. For retrieval, measure recall@K: of the items the user actually engaged with next, how many were in the candidate set. For ranking, AUC, log loss and NDCG on a time-split holdout (train on days 1-28, test on day 29, never random splits that leak the future). But offline gains often do not translate online because logs only contain items the old system showed. The real decision comes from A/B tests on the business metric: orders or revenue per user for commerce, watch time or retention for video. Watch guardrails: latency, return rate, diversity, complaint rate, seller concentration. Run tests long enough to cover weekday/weekend cycles and beware novelty effects. Optimising only CTR leads to clickbait; combine predicted click, purchase and satisfaction (e.g. not returned) into the final score with tuned weights.
8 Trade-offs
Precompute recommendations vs compute online
Precomputing top-N per user nightly is cheap to serve but ignores the session and wastes work on inactive users. Online computation reacts to clicks in seconds but needs low-latency features and models. Common hybrid: precompute candidates and embeddings, rank online.
Gradient-boosted trees vs deep ranking models
GBDTs (LightGBM, XGBoost) are fast on CPU, strong on tabular features and easy to debug: a good v1. Deep models (DLRM, DCN, transformers over sequences) win at large scale with sparse IDs and sequence features but need GPUs and more MLOps.
Freshness vs stability
Real-time session features make recs responsive, but too much weight on the last click makes the feed jumpy. Blend long-term and session embeddings and cap session influence.
Single objective vs multi-objective
Optimising one signal (click) is simple but leads to clickbait. A weighted combination of p(click), p(purchase) and p(satisfied) aligns with business goals; the weights become product decisions tested in A/B.
9 Bottlenecks
Ranking compute at peak
Scoring 500 items x 50k QPS is heavy. Cut candidates per stage, cache user features per session, batch requests on GPU servers, and use a distilled smaller model at peak. Cache the full result for 30-60 s per user per surface.
Feature store hot keys
Popular items' features are read by almost every request. Keep a local in-process cache of item features with a short TTL; only user features need a remote read per request.
Training data growth
Impressions grow with traffic. Downsample negatives (impressions without clicks) and correct with weights; keep a rolling window.
10 Failure handling
Ranking model server down or slow
Orchestrator has per-stage deadlines. If ranking misses its 40 ms deadline, return candidates ordered by retrieval score; if retrieval fails, serve a precomputed popular-in-city list from cache.
Bad model pushed
Canary on a small traffic slice with automatic guardrail checks (CTR collapse, latency, empty results). Model registry keeps the previous version for instant rollback.
Streaming feature pipeline lags
Features carry an updated_at; the ranker treats stale session features as missing (the model is trained with missing values), so quality degrades gently instead of using wrong data.
11 What impresses
- Giving funnel sizes at each stage with latency budgets
- Explaining training-serving skew and logging served features
- Mentioning feedback loops and exploration with propensity logging
- Time-based train/test splits, not random
- Choosing the business metric and guardrails before the model
- Graceful fallback chain: ranked -> retrieval order -> popular cache
12 Mistakes
- Jumping to 'use collaborative filtering' without a serving architecture
- Scoring all items with one heavy model per request
- Ignoring cold start for new users and new items
- Only offline metrics, no A/B testing plan
- Recommending out-of-stock or already purchased items
- Optimising only for clicks
13 Follow-ups
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.