Part VI · Design · chapter 19 of 19
System design
The last chapter collects the previous eighteen into a single procedure. In an interview «design a recommender system for X» is not a question about knowing architectures but about the order of thinking: where you start, what you fix before naming a single model, and at which moment you admit that the first solution will have to be thrown away.
- First a simple working solution, then criticism, then the full one. A complex system designed from scratch does not work and cannot be patched into working.
- Goal setting cannot be skipped. A crude mistake here will not be rescued by any model below it — and skipping it is always tempting.
- The size of the catalogue determines the architecture. Up to 75 000 candidates no funnel is needed at all, and refusing one is the right decision, not a simplification.
- A rare target event is measured through a proxy not out of stubbornness. Catching the same relative effect at a conversion of 0.5% instead of 40% costs 133 times more traffic.
1. Gall's law
«A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work».
Hence the general shape of an answer: first build a simple but working solution → criticise it → build a fuller one.
And the subtlety that keeps this from being an empty slogan: the criticism leans on those features of the problem that we came to understand while building the simple solution. That is, the simple solution is needed not only as a fallback — it is needed as an instrument of understanding.
They are part of the life of any engineer and any runtime. They are best accepted with an understanding of what the long-term correct solution is — so as not to do something that fundamentally contradicts it.
An honest caveat: sometimes merely working out the correct solution is extremely expensive. Then it is better to trust experience — some working solution is in any case better than none.
2. Five steps
Step 1: goal setting
- Find the key business metric we want to grow.
- Tie it to an offline metric a model can grow.
- Determine the key target of the model.
A crude mistake here will not be rescued by any complex model below. If you are optimising the wrong thing, the quality of the implementation does not matter.
And skipping it is always tempting: many people are impatient to start doing something already, and often that is a useful trait — which is exactly why goal setting looks like the most logical candidate for a quick skip.
Partly to compensate for that temptation we build the simple solution first: to reach something working quickly and come back to the goal with an understanding of the problem.
Step 2: constraints
A purely brainstorming stage — it prepares the material basis for every decision that follows. What to collect:
- the sizes of the catalogue and of the user base;
- requirements on availability, filtering, and other product constraints;
- separately — the requirements on response time;
- how the user's intent is arranged;
- the main biases the system is most exposed to.
The size of the catalogue determines the architecture more than anything else. Let the ranking model spend 4 µs per candidate and the response budget be 300 ms:
| Catalogue | Full scan | Does it fit? | Selection needed by |
|---|---|---|---|
| 3e+03 | 12.0 ms | yes | — |
| 5e+05 | 2000.0 ms | NO | 7 times |
| 5e+08 | 2000000.0 ms | NO | 6667 times |
The threshold where a full scan stops fitting: 75 000 candidates.
The numbers are reproduced by the script _tools/design_demo.py in this repository.
Below the threshold candidate generation is not needed at all — and that is not a simplification but the right decision: any funnel loses candidates, and the ceiling of quality is set by the first stage. A candidate who begins an answer with «first we build a two-tower retrieval» without asking about the size of the catalogue shows that they are applying a template rather than thinking.
Steps 3–4: technology and ML
Formulate the key actors — most often the user, the item and the context. Then invent ideas for candidate generation and for features:
- for each actor separately — three cells;
- for each pair of actors — three more.
The result is a 3 + 3 grid that almost always covers everything essential. And it is convenient to speak through out loud as the structure of the answer — the interviewer sees a system rather than an enumeration.
| Actor or pair | Example features |
|---|---|
| User | purchase frequency (with a time limit, so the features «travel» less), average basket, embeddings of favourite categories |
| Item | sales of the good, of its category and of its manufacturer over \(X\) days; impressions, CTR, CVR and their weighted versions; the embedding |
| Context | day of the week, seconds since the start of the day, the embedding of the surface, the embedding of the basket |
| User × item | the dot product of the towers; how many times the user took this good, this category, this manufacturer over \(X\) days |
| User × context | how much the user usually buys on this day and hour; how typical the current basket is for them |
| Item × context | how often the good is taken at this time; its CTR and CVR at this time; the closeness of the good to the basket |
Step 5: the experiment
- The design of the A/B: which split, which criterion, the power and the significance level, the required length, the interesting slices.
- The key metrics: fix the list, leaning on the goal setting, and make it wider for ease of interpretation.
- Guardrail metrics: the ones that will help understand what is happening when something inevitably goes wrong.
Fix the concrete hypothesis you are testing. Otherwise, with a large number of metrics, you will always find something pleasant that turned significant — and the experiment turns into a search for confirmations instead of a test.
And before launching it is worth estimating whether there is enough traffic: the sample size and the MDE are computed in advance, not after the experiment failed to turn significant.
3. Case 1: e-grocery
The anchor example, against which the differences are easier to show afterwards.
Product ones. Recommendations on very many surfaces, which actively cannibalise each other. The user is well known: a large share of regular customers, nothing works without a login — cold start is less relevant. And the user already has an intent: almost nobody comes in «just to scroll». The task is not to get in the way too much while selling something else along the road.
Technical ones. The catalogue at a location is most often up to 3000 items and well structured. There are tens of millions of users and hundreds of requests per second. The response budget, counting prefetches, is up to 300 ms. Goods become available and unavailable instantly, and they spoil, so new arrivals have to appear in the output quickly.
The simple first approach. The key business task is the growth of turnover. Offline we proxy it with NDCG where the gain is the price of the good. Hence the obvious target — a purchase — and the score is assembled as
$$ \text{score} = p(\text{buy}\mid \text{show}) \cdot \text{price} $$or the price is sewn straight into the ranking loss. The dataset follows explicitly: a purchase against an impression in the slate.
And now the criticism — that same second pass through the scheme:
- money now may not correspond to money in the long run; the business would rather grow the sum over users on a long horizon;
- the metric does not account for users buying several identical goods;
- and it does not account for the cannibalisation of surfaces at all.
Note: not one of the three problems is fixed by improving the model. All three are about the formulation.
Two surfaces, each giving +10 units of turnover in isolation:
| Overlap of audience and assortment | 0.0 | 0.2 | 0.5 | 0.8 | 1.0 |
|---|---|---|---|---|---|
| total gain | +20.0 | +18.0 | +15.0 | +12.0 | +10.0 |
| loss against the sum | 0% | 10% | 25% | 40% | 50% |
The numbers are reproduced by the script _tools/design_demo.py.
At full overlap the second surface adds nothing: the user would have bought the same thing without it. An A/B of each surface separately will show a sum of 20, while a joint launch gives less — and the difference grows the closer the surfaces are.
This is a classic reporting trap: the teams honestly show their gains and the total turnover does not grow. It is cured by metrics at the level of the user rather than the surface, and by an uplift formulation — «a purchase on this surface relative to a purchase at all».
| Decision | Why so |
|---|---|
| No sharding | with such a catalogue and such load there is no point in it |
| A static profile store, refreshed once a day | users rarely buy more often than once a day |
| A static item index, built once a day | the arrival of a new good is known in advance |
| A cache of available goods right on the service + a follow-up filter | filtering is critical: availability changes instantly |
| A filter by the current basket and by legal restrictions | do not show what has been added; tobacco, alcohol |
It is useful to name that yourself, without waiting for the question.
- A user may assemble a basket over a long time. Then actions inside the session become important — especially for those we know less about. Which means a runtime loop for the user part is needed.
- The solutions against cannibalisation will require a full shared feature layer.
- Growth of the catalogue will require splitting candidate generation and features into separate services.
Re-ranking: the hygienic minimum is softmax sampling against the feedback loop, plus categorical diversity and separate work with new arrivals through bandit blending.
The experiment: the split is by user; the key metric is integral turnover; the auxiliary ones are margin and the conversion of the surface; the guardrails are turnover by category (did we skew the output) and conversion by position (did we break the top).
4. Case 2: a streaming service
From here on, only what differs significantly from the anchor case.
- A catalogue of 300–500 thousand: candidate generation is definitely needed, sharding is not yet.
- All consumption is built around a single item: if they are watching an episode, they will almost certainly go on watching that series. So a runtime user profile may not even be needed.
- Many regional restrictions and kinds of subscription — they must be taken into account at an early stage of filtering.
- Children's content litters the history — in practice it is most often simply removed.
The business needs subscriptions. But the number of subscriptions is a bad metric for an A/B: the event is very rare and will not turn significant in a short experiment. Exactly how bad:
| Base conversion | Needed per group | More expensive than at 40% |
|---|---|---|
| 0.400 | 9.42e+03 | 1.0 |
| 0.200 | 2.51e+04 | 2.7 |
| 0.050 | 1.19e+05 | 12.7 |
| 0.010 | 6.22e+05 | 66.0 |
| 0.005 | 1.25e+06 | 132.7 |
The numbers are reproduced by the script _tools/design_demo.py; a relative gain of 5%, significance 0.05, power 80%.
The number of observations is proportional to \((1-p)/p\) at a fixed relative effect. So catching the same effect at a conversion of 0.5% instead of 40% costs 133 times more traffic.
A proxy metric is taken not out of an analyst's stubbornness — a direct measurement simply does not fit into any reasonable experiment.
- Pick a list of base metrics with a plausible long-term effect: watching series (they have better retention), discovery and the diversity of what is consumed, frequency of use.
- Build a dozen candidate combinations out of them.
- Work out retrospectively which of them show the long-term effect better — by matching similar users or by adjusted correlations. A model can be trained too, but interpretability is needed.
- Validate on a golden set of past experiments — those A/Bs whose long-term outcome is already known.
The fourth point is the one usually forgotten, and it is the real validation: without it the proxy remains a hypothesis.
There are many cold users, so onboarding is needed. The naive scheme: show the top of the popular, the user picks, recompute the top. The problem is that the top barely changes.
The trick: after a choice, remove from the sample all the users who also liked what was chosen, and rebuild the top. Let us simulate on an audience of five taste groups of different sizes, five questions:
| Scheme | Which groups were asked about | Groups covered |
|---|---|---|
| naive, the pool does not change | 0, 0, 0, 0, 0 | 1 of 5 |
| remove those who liked it | 0, 1, 2, 3, 4 | 5 of 5 |
The numbers are reproduced by the script _tools/design_demo.py.
The naive scheme asks five times in a row about one and the same taste: the largest group does not go anywhere, and every next question brings no new information. Removal turns the top into «the top for those who did not like the previous one» — and in five questions we go round all the groups.
The formulation worth carrying away: we choose the next question so that it splits the remaining uncertainty as much as possible, not so that it is the most popular.
A carousel interface adds a story of its own: the output consists of blocks — «continue watching», «similar to…», genre selections. One can choose the blocks and rank inside them, or generate a common pool and slice it; in practice both are done. The outer ranking between blocks is often bandit-based because of heterogeneity: comparing «continue watching» with a genre selection using one model is hard — they have a different nature and a different scale of response.
5. Case 3: a media feed
Scale changes everything.
- An index of hundreds of millions of items: sharding is absolutely certainly needed.
- There is no explicit intent — people come to look at «something». Consumption is cheap and short, and a runtime loop for features is definitely needed.
- Not all items are trustworthy — this is user-generated content, so a moderation pipeline and instant bans are needed.
- Degradation and freshness: trends appear and vanish fast, models go stale noticeably quicker than in the previous two cases. Online fine-tuning and retraining pipelines help.
This is the main difference of the case, and it changes the formulation. The author has their own interests and expectations: authors count on impressions, and their absence produces complaints. The system stops being two-sided «user — item» and becomes three-sided.
The typical approaches: item exploration built into the model, PID controllers to stabilise the volume of impressions, explicit quotas.
And the consequence people forget: authors are paid for impressions, so fraud begins — bots are run, CTR is inflated. In a good system the target is antifraud-aware, as is the objective metric, and the teams work closely together.
A subtlety worth naming in an interview. On large interfaces — a feed, selections — long sessions can be optimised. On small ones — one clip on the screen, a carousel — rather not: long articles and videos will be skipped, and we will get artefacts in the metric.
Multimodality adds more: articles, videos and short clips are mixed into one feed, so both the representations and the metrics have to be comparable across content types. «Watched to the end» for an article and for a clip are different events with different prices.
6. Three systems side by side
| E-grocery | Streaming | Media feed | |
|---|---|---|---|
| Catalogue | ~3 000 at a location | 300–500 thousand | hundreds of millions |
| Candidate generation | can go without it | needed, no shards | needed + sharding |
| Intent | strong, a purchase | from a single item | absent |
| Cold start | less relevant | critical for users | critical for items |
| Business goal | turnover → NDCG with gain = price | subscriptions → a proxy from watch time | attention + the happiness of authors |
| The key pain | cannibalisation of surfaces | a rare target event | scale, moderation, fraud |
Note that not a single row is about the model. The ranking architecture in all three cases will be similar; what distinguishes the systems is everything else — and that is exactly what is asked about.
How to lay 300 ms out over the stages — it is useful to keep the orders of magnitude in mind:
| Stage | ms | Share |
|---|---|---|
| network and parsing the request | 20 | 6.7% |
| the user profile | 30 | 10.0% |
| candidate generation | 40 | 13.3% |
| filtering | 15 | 5.0% |
| features | 60 | 20.0% |
| ranking | 80 | 26.7% |
| re-ranking and blending | 25 | 8.3% |
| serialisation and the network back | 20 | 6.7% |
| in total | 290 | 96.7% |
The numbers are reproduced by the script _tools/design_demo.py.
The margin is 10 ms, and all of it will go on the tail of the distribution: the budget has to be held at p99, not at the mean. And note where the money is: features and ranking eat 47%, so speed-ups are looked for there first, rather than in candidate generation, where people instinctively look first.
7. The interview checklist
- Which business metric? And immediately — how often the target event happens. If it is rare, the conversation about a proxy starts here, not at the end.
- The size of the catalogue and the number of users? That decides whether a funnel is needed at all.
- Does the user have an intent? A feed and a search are different problems, though formally one.
- The response budget? 50 ms and 300 ms are different architectures.
- Who is known: users, items, both? Hence which cold start is critical.
- Is there a third side — authors, sellers, advertisers? That changes the formulation entirely.
- Which filters are mandatory? Legal restrictions are designed in from the very beginning, not bolted on.
- Starting with the architecture. «We will take a two-tower retrieval and a DCN on top» — before the size of the catalogue has been established. It shows a template instead of thinking.
- Skipping goal setting. The most expensive mistake: nothing below in the stack will rescue it.
- Not naming where the solution will break. A good answer contains its own criticism; waiting for it from the interviewer is a missed opportunity.
- Forgetting the experiment. A system that cannot be measured has not been designed.
- Not naming the guardrail metrics. A sign that the person has not launched anything into production: there, something always breaks somewhere nearby.
Interview questions
Design a recommender system for X. Where do you start?
With five steps in this order: goal setting (business metric → offline metric → the model's target), constraints (catalogue, users, SLA, filtering, intent, biases), the technical architecture, the ML architecture, the experiment setup. Then criticism of what came out and a second pass.
The justification is Gall's law: a complex system that works has always evolved from a simple one that worked, while one designed from scratch does not work and cannot be patched. And the simple solution is needed not as a fallback but as an instrument of understanding: we will criticise on the basis of what we came to understand while building it.
The first things I will ask before any architecture: which business metric, how rare the target event is, the size of the catalogue and the response budget.
Why can goal setting not be skipped?
Because a crude mistake at this stage will not be rescued by any model below: if the wrong thing is being optimised, the quality of the implementation does not matter.
And it is exactly the stage one wants to skip: many people are impatient to start doing things, and usually that is a useful trait — which is why it is dangerous precisely here. A partial compensation is to build the simple solution first, to reach something working quickly and come back to the goal already understanding the problem.
How do you tell whether candidate generation is needed?
Compute it. If the ranking model spends around 4 µs per candidate and the budget is 300 ms, a full scan fits up to roughly 75 thousand candidates. A catalogue of 3000 items is scanned in 12 ms — no funnel is needed at all. A catalogue of 500 thousand requires a selection by 7 times, hundreds of millions by thousands of times.
And it matters that refusing the funnel on a small catalogue is not a simplification but the right decision: any funnel loses candidates, and the ceiling of quality is set by its first stage.
The business needs subscriptions. Why can subscriptions not be measured?
Because the event is too rare. The number of observations for a fixed relative effect is proportional to (1−p)/p, so catching the same gain at a conversion of 0.5% instead of 40% costs 133 times more traffic: 1.25 million per group against 9.4 thousand.
A proxy metric is needed — sensitive in a short A/B and correlated with the long-term goal. It is built like this: choose base metrics with a plausible long-term effect (watching series, discovery, frequency of use), assemble candidate combinations out of them, select the best retrospectively and always validate on a golden set of past experiments with a known outcome. The last step is the real validation.
How do you generate feature ideas quickly?
With a 3 + 3 grid. Write out the actors — usually the user, the item and the context — and invent features for each one separately and for each pair.
For example: the user — purchase frequency and average basket; the item — sales and CTR over X days; the context — day of the week and the embedding of the surface; user × item — the dot product of the towers and how many times they took this category; user × context — how much they usually buy at this hour; item × context — the good's CTR at this time.
The grid almost always covers what is essential, and it is convenient to speak through out loud as the structure of the answer.
What is surface cannibalisation and what do you do about it?
The situation where two surfaces show one user an overlapping assortment and their gains do not add up. If each in isolation gives +10, then at half overlap together they give +15 rather than +20; at full overlap +10, that is, the second adds nothing.
A classic reporting trap: the teams honestly show the gains of their surfaces while the total turnover does not grow. It is cured by metrics at the level of the user rather than the surface, and by an uplift formulation — a purchase on this surface relative to a purchase at all. And uplifts require a full shared feature layer.
How do you onboard a cold user?
Show the top of the popular, let them choose — and then the key trick: remove from the sample all the users who also liked what was chosen, and rebuild the top.
Why: without the removal the top barely changes and the next question brings no new information. In a simulation on five taste groups the naive scheme asks five times in a row about one and the same group — 1 of 5 covered; with removal all 5 are covered.
The general formulation: we choose the next question so that it splits the remaining uncertainty as much as possible, not so that it is the most popular.
What changes when authors appear in the system?
The system stops being two-sided and becomes three-sided. Authors have their own expectations: they count on impressions, and their absence produces complaints. The instruments are item exploration inside the model, PID controllers to stabilise the volume of impressions, explicit quotas.
And the consequence people forget: authors are paid for impressions, so fraud appears — bots and inflated CTR. The target and the objective metric have to be antifraud-aware, otherwise the system will conscientiously optimise the inflation.
How do you lay the response budget out over the stages?
The orders of magnitude at a budget of 300 ms: network and parsing 20, the profile 30, candidate generation 40, filtering 15, features 60, ranking 80, re-ranking 25, the answer 20 — 290 in total, a margin of 10 ms.
Two conclusions. First: the margin will go on the tail of the distribution, so the budget is held at p99 rather than at the mean. Second: features and ranking eat 47% of the budget, and speed-ups should be looked for there first, not in candidate generation, where people look instinctively.
One-screen cheat sheet
Gall's law
Simple and working → criticism → full. The simple solution is an instrument of understanding.
Five steps
Goal → constraints → technology → ML → experiment. And a second pass.
The first number
The scan threshold ≈ 75 000 candidates. Below it a funnel is neither needed nor useful.
A rare event
n ∝ (1−p)/p. 0.5% against 40% — 133 times more traffic. Hence a proxy.
The 3+3 grid
User, item, context — separately and pairwise. It covers what is essential.
Cannibalisation
At full overlap the second surface gives zero. Measure per user.
Onboarding
Remove those who liked it: 5 groups of 5 instead of 1 of 5.
The budget
Features and ranking — 47%. Hold it at p99, not at the mean.
Primary sources
- J. Gall. Systemantics: How Systems Really Work and How They Fail, 1975 — the source of Gall's law.
- C. Gomez-Uribe, N. Hunt. The Netflix Recommender System: Algorithms, Business Value, and Innovation, TMIS 2015 — the carousel interface and the link to business goals.
- H. Steck et al. Deep Learning for Recommender Systems: A Netflix Case Study, AI Magazine 2021.
- D. Sculley et al. Hidden Technical Debt in Machine Learning Systems, NeurIPS 2015.
- R. Kohavi, D. Tang, Y. Xu. Trustworthy Online Controlled Experiments, Cambridge 2020 — proxy metrics, golden sets of experiments and guardrail metrics.
- The numbers in this chapter:
_tools/design_demo.pyin this repository.
Nineteen chapters followed the path of a request: from the statement of the problem and its measurement — through candidate generation, ranking, work with sequences and with the slate — to engineering and back to design.
If one thought is worth taking away from all of it, let it be this: in recommendations almost every technical decision is derived from a constraint rather than from fashion. The funnel comes from the latency budget. Factorisation comes from sparsity. Two towers come from the number of candidates. The stochasticity of the output comes from the need to measure. A good interview answer differs from a bad one not in the set of names but in whether the person names the constraint before the solution.
Next come the revision questions, the widgets and the walkthrough of the open-sourced code of a real feed, where all of this is visible in working form.