How to Design a Recommendation System (The MLE Interview Framework)
Every senior MLE interview at Spotify, Netflix, Meta, or Airbnb eventually lands on a recommendation system design question. The surface area is enormous: candidate generation, ranking, serving, monitoring, cold start, exploration. Here's the framework that works.
There are six components to any recommendation system design. Miss one and you'll look junior. Cover all six and you look like someone who has shipped this before.
1. Problem framing (5 minutes)
This is where most candidates lose points immediately. They jump to architecture before establishing what the system is optimising for. Ask:
These answers fundamentally change everything downstream.
2. Candidate generation (retrieval)
At scale (10M+ items), you cannot rank all items for every user. Retrieval narrows the candidate set to ~100–1000 items that are plausibly relevant.
Methods: Collaborative filtering (matrix factorisation), content-based (item embeddings), two-tower model (user embedding + item embedding → ANN lookup), popularity + filters.
Two-tower is the industry standard for large-scale retrieval. User tower: encodes user history, demographics, context. Item tower: encodes item metadata, engagement signals. Train with in-batch negatives. Serve by indexing item embeddings in FAISS or ScaNN.
3. Ranking
Given ~500 candidates, rank them. This is where you can afford more expensive models.
Options: GBDT (XGBoost/LightGBM) for tabular features — interpretable, fast, doesn't need GPUs. Deep neural networks for feature interaction learning. LTR (Learning to Rank) if you have explicit relevance labels.
Features for ranking: user-item interaction history, item quality signals (CTR, completion rate, rating), freshness, context (time of day, device, location), social signals (friend interactions).
4. Post-processing
Raw ranking scores are not what you serve. Apply:
5. Serving infrastructure
Online serving: user embedding lookup from feature store → ANN retrieval → candidate features from feature store → ranker inference. Total budget: typically <100ms P99.
Key latency sources: feature lookup (~5ms Redis P50), ANN retrieval (~10ms with FAISS), ranker inference (~20ms GPU batch). Sum to 35ms with headroom.
6. Monitoring
Engagement metrics (CTR, completion rate, time spent) — leading indicators. Satisfaction metrics (ratings, explicit feedback, return visits) — lagging but reliable. Diversity metrics (ILD, coverage, long-tail ratio) — prevents filter bubbles. Data quality: feature freshness, null rates, embedding staleness. Model performance: prediction score distribution drift, AUC on logged feedback.
```python import numpy as np
class TwoTowerScorer: """Minimal two-tower scoring at inference time."""
def __init__(self, user_tower, item_tower): self.user_tower = user_tower # returns 128-dim embedding self.item_tower = item_tower # returns 128-dim embedding
def get_user_embedding(self, user_features: dict) -> np.ndarray: return self.user_tower.predict([user_features])[0] # (128,)
def score_candidates(self, user_emb: np.ndarray, item_ids: list) -> list[tuple]: """Dot-product scores for a candidate set. O(k * d).""" item_embs = self.item_tower.predict(item_ids) # (k, 128) scores = item_embs @ user_emb # (k,) ranked = sorted(zip(item_ids, scores), key=lambda x: x[1], reverse=True) return ranked # [(item_id, score), ...]
# Production note: item embeddings are pre-computed and stored in # an ANN index (HNSW/ScaNN). You never score the full catalog — # you retrieve top-1000 candidates, then rerank with a heavier model. ```