TL;DR: Two stages: a candidate generator that mixes collaborative-filtering embeddings with a content-based tower so brand-new courses and brand-new users are still retrievable, then a ranker optimizing conversion (enrollment) with completion and refund guardrails, not raw clicks. Use session and intent signals to make logged-out and day-one users rankable, and apply an explicit diversity plus business-rule layer before the final list.
How to approach it
The binding constraint is signal sparsity: a large share of traffic is logged-out or first-session, and the catalog turns over as new courses launch weekly, so a pure collaborative-filtering system has nothing to say about the items and users that matter most commercially. Ask three things up front: what fraction of sessions are cold (no history), what is the business objective (enrollment, revenue, completion, retention), and what latency budget the surface allows. Then design candidate generation and ranking as separate stages so you can fix retrieval and scoring independently, and decide the objective before you decide the model.
A strong answer
Split the system into candidate generation and ranking. Candidate generation reduces a catalog of, say, 200k courses to a few hundred plausible ones in single-digit milliseconds; ranking scores those few hundred with an expensive model. Conflating them is the classic mistake: you cannot run a heavy ranker over the whole catalog, and a cheap retriever over the whole catalog gives bland results.
Candidate generation should blend sources, not lean on one model:
- Collaborative filtering via two-tower embeddings (a user tower and an item tower) trained on co-enrollment and co-view, served by approximate nearest-neighbor search (HNSW or FAISS). This is the workhorse for users and items that have history.
- Content-based retrieval for cold items and cold users. The item tower consumes title, description, syllabus, instructor, category, level, and a text embedding, so a course that launched an hour ago is retrievable before anyone has touched it. This is the single most important decision in this question.
- Session and co-occurrence sources: "people who viewed this also viewed", trending-in-category, and recently-viewed-to-next. These carry the most weight in a logged-out or first-session context.
The ranker is a gradient-boosted tree or a small DNN scoring each candidate on features that span three buckets: user (skill level, past categories, price sensitivity, device), item (rating, enrollment volume, recency, price, completion rate), and crucially the user-item cross features and session context (time on category page, search query that led here, items already in cart). The cross features are where the lift comes from; a model with only user and item features in isolation is a popularity sorter.
Label design is where this question is won or lost. Do not train on clicks alone. A click means "the thumbnail worked", not "this course was worth buying". Use a graded label: enrollment is a strong positive, add-to-cart a weak positive, click a very weak positive, and a refund within the guarantee window a hard negative even though it followed a purchase. For courses, completion or first-module progress is a quality signal worth folding in, because optimizing pure enrollment will happily recommend a cheap, badly-rated course that converts and refunds. Weight labels by their downstream value rather than treating every interaction as 1.0.
Pick the objective deliberately. Engagement (clicks, dwell) is easy to move and easy to game; conversion (enrollment, revenue) is the business metric but sparse; completion and retention are the long-term metric but slow to measure. Optimize predicted enrollment as the primary objective, but carry completion-rate and refund-rate as guardrails so a model that lifts enrollment by recommending junk gets caught. State this explicitly: "primary metric enrollment-per-session, guardrails refund rate and 30-day completion, ship only if primary improves and no guardrail regresses".
| Objective | Pro | Con | Use as |
|---|---|---|---|
| Clicks / CTR | Dense, fast feedback | Rewards clickbait thumbnails | Weak feature, never the goal |
| Enrollment / revenue | The business metric | Sparse; can reward refund-prone junk | Primary objective |
| Completion / retention | Real long-term value | Slow (weeks to observe) | Guardrail + reweighted label |
For cold start, separate the two cases. A cold item is solved by the content tower plus a deliberate exploration budget: reserve a small slice of impressions (an epsilon-greedy or Thompson-sampling slot) to show new courses and gather the interactions that let collaborative filtering pick them up within days, otherwise new items never accumulate the signal that would rank them and you starve your own catalog. A cold user is solved by session and intent signals: the entry query, the category page, and the first one or two clicks let a content-based ranker personalize within a session, falling back to trending-in-category and editorial picks before any click exists.
Diversity and business rules sit between the ranker and the response. Pure relevance sorting collapses into ten near-duplicate Python courses, so apply maximal-marginal-relevance or per-category caps to spread the list. Then layer business rules the model should not learn implicitly: do not recommend a course the user owns, respect promotional placements and contractual instructor boosts, filter region-restricted or deprecated content, and cap any single instructor's appearances. Keep these as a transparent post-ranking layer, not features, so the business can change a rule without retraining.
Online evaluation is the real gate. Offline, track recall@k and MAP for retrieval and AUC/NDCG for ranking, but offline metrics only tell you the new model agrees with logged behavior, which is biased by the old model. Validate with an A/B test: primary metric enrollment-per-session, guardrails on refund rate, 30-day completion, catalog coverage (to catch popularity collapse), and latency. Run long enough to see refunds, which lag clicks by days. The "CTR went up, refunds went up" scenario is exactly why you measure conversion and refunds together rather than declaring victory on engagement.
Watch the feedback loop: the model trains on what it showed, so it amplifies its own choices and a niche category can quietly vanish from training data. Mitigate with the exploration budget above, by logging propensities for inverse-propensity reweighting, and by monitoring coverage so you notice the catalog narrowing before the business does. On latency, ANN candidate generation is single-digit milliseconds and the ranker over a few hundred items is tens of milliseconds; precomputed user embeddings and cached trending lists keep the cold path fast, for around 100ms p99 on a landing surface.
What interviewers probe next
- "Your CTR went up but refunds also went up. What happened?" The model learned to optimize clicks, not value. Move the objective to conversion, add refund and completion guardrails, and reweight labels so a refund is a hard negative. Engagement was a proxy that the model gamed.
- "A new course gets zero traffic for weeks. Why, and how do you fix it?" Collaborative filtering cannot rank what it has never seen, so without a content tower and an exploration budget new items are invisible by construction. The content tower makes them retrievable and the exploration slot earns them the interactions CF needs.
- "How do you recommend to a logged-out first-time visitor?" Session intent is the signal: the entry query, the category page, and the first clicks drive a content-based ranker within the session, with trending-in-category and editorial picks as the zero-click fallback.
Common mistakes
Training the ranker on clicks and calling it conversion, then being surprised when revenue does not move. Treating cold start as a footnote instead of designing the content tower and exploration budget that make it work, which is the single thing that distinguishes this from a textbook recsys answer. Running one model over the whole catalog instead of separating candidate generation from ranking. Sorting purely by relevance and shipping ten near-identical courses. Declaring success on an offline NDCG that just measures agreement with the biased logs of the old system. And ignoring the feedback loop, so the system narrows to a handful of popular items and the business notices coverage collapse before you do.
Key takeaways
- Separate candidate generation (ANN over a blend of CF, content, and session sources) from ranking; never run one model over the whole catalog.
- Solve cold start explicitly: a content-based tower makes new items and users retrievable, an exploration budget earns new items the signal CF needs.
- Optimize conversion with completion and refund guardrails, not clicks, and design graded labels where a refund is a hard negative.
- Validate online with an A/B test on enrollment plus guardrails (refunds, completion, coverage, latency), and watch the feedback loop for catalog collapse.
