InterviewKata
System design · Worked design D41 of 46 · Data and infrastructure · Long form

Design an ad serving system

Design the system that picks which ad to show when a page or app has a slot to fill: find the campaigns allowed to show to this person here, predict how likely each one is to be clicked, run an auction, respect every advertiser's budget and frequency limits, and answer in tens of milliseconds, half a million times a second. Counting the clicks afterwards is D17; this is the decision before.

1. The prompt

"Design the ad serving system for a large app: advertisers create campaigns with targeting and budgets; when a user opens a page with an ad slot, choose the ad."

What makes it hard:

2. Frame the problem

Whose ads, whose slots?

Our own app's slots (feed and search results), filled by advertisers who buy through our self-serve tools. No external exchanges for now.

A first-party ad network: we run the auction and know the users.

How do advertisers pay?

Mostly per click (CPC) with a maximum bid; some per thousand impressions (CPM).

To compare them we convert everything to expected value per impression (eCPM).

Scale?

500,000 ad requests a second at peak; 5 million active ads in 1 million campaigns; 10 million campaign changes a day.

Too many ads to score them all per request: we need retrieval first, then ranking.

Latency?

The ad server must answer within 50 ms at p99 (the rest of the 100 ms goes to the network and rendering).

Every stage gets a budget, and slow stages are cut off, not waited for.

Budget and pacing?

Daily budgets per campaign, spent evenly through the day; overspend under 1–2%.

Spend must be tracked in near real time across hundreds of ad servers.

Frequency caps?

Yes: e.g. "show this ad to a person at most 3 times a day".

Needs a per-user, per-campaign counter read on every request.

3. What it must do

  1. Choose an ad for a slot: only eligible ads (targeting, budget, frequency cap, policy), ranked by expected value, chosen by auction.
  2. Charge correctly: decide the price the winner pays, and record impressions and clicks for billing.
  3. Pace budgets so campaigns spend through the whole day, never far beyond their budget.
  4. Apply campaign changes (new ads, paused campaigns, new bids) within a minute.

Not now: the advertiser UI and reporting, creative review, external exchanges (real-time bidding with other networks), click fraud detection (part of D17).

4. How well it must do it

TargetWhy this number
Decision within 50 ms at p99 speedA late answer is worth nothing.
Overspend under 2% of daily budget moneyOverspend is usually not billed, so it's revenue given away and an unhappy advertiser.
Campaign changes live within 60 s; a pause stops spend within 60 s freshnessAdvertisers expect "pause" to mean pause.
Availability 99.95% surviving failuresEvery failed request is lost revenue; fall back to a house ad rather than an error.

5. Numbers that change the design

QuantityWorking→ so we…
Ads to consider5 M ads; scoring one with the model takes ~10 µs, so all of them would take 50 s→ so retrieval narrows to ~1,000 eligible candidates with an index, a light model trims to ~100, and the heavy model scores only those
Requests500,000/s × 100 heavy-model scores = 50 M predictions a second→ so model serving is its own fleet (batched, often on GPUs), and the candidate count is a budget, not a constant
Spend updates500,000 impressions/s, each changing one campaign's spendA single counter per campaign would be hot → so each ad server spends from a local allocation and reports in batches
Frequency counters400 M users × a few active campaigns each→ so caps live in a low-latency key-value store keyed by user, read once per request for all campaigns

Illustrative numbers.

6. The nouns

campaign       id, advertiser_id, daily_budget, schedule, targeting {age, country, interests[], placements[]}, bid {type: cpc|cpm, max}
ad             id, campaign_id, creative_url, status
targeting idx  attribute value → posting list of ad ids        (in memory on every ad server)
spend          campaign_id → spent_today, allocation per ad server (pacing service)
freq           (user_id) → {campaign_id: shows_today}            (key-value store, TTL 1 day)
auction log    auction_id, request, candidates, scores, winner, price   (stream → data lake)

7. The contract

Internal: the app's backend calls
  POST /v1/ads:select   {slot_id, user_id, context {page, country, device}, count: 1}
    → {ad_id, creative, auction_id, impression_url, click_url}   or 204 (no ad: show house content)

Tracking (from the client)
  GET  /i/{auction_id}   impression rendered     → recorded once (deduplicated by auction_id)
  GET  /c/{auction_id}   click                   → redirect to the advertiser's link

Advertiser API (separate system)  campaigns, ads, budgets, bids → change events

The auction_id ties every impression and click back to the decision, the price and the model version, which is what billing, reporting and training all need.

8. First working version

A fleet of stateless ad servers, each holding the whole targeting index in memory, calling a model-serving fleet for predictions and a key-value store for frequency counts. A pacing service hands out spend. Every decision is logged to a stream.

8.1 One ad request, step by step

8.2 The auction

For each candidate, eCPM = bid per click × pCTR × 1,000 (for CPM bids, the bid itself). The highest eCPM wins. A CPC bid of £2.00 with a 1% predicted click rate is worth £20 per thousand impressions; a £0.50 bid with 5% is worth £25 and wins.

What the winner pays depends on the auction type. In a second-price auction, they pay just enough to have beaten the runner-up, which makes bidding your true value the best strategy. In a first-price auction they pay their own bid, and advertisers' tools then shade bids down. Many display exchanges moved to first-price around 2019; a single-owner network like ours can choose either, and second-price (with a quality-adjusted rank) is the classic choice for self-serve CPC ads. Try both in the lab in section 13.

Parked for laterretrieval at 5 M ads (9.1) · the prediction budget (9.2) · budget pacing (9.3) · frequency caps (9.4) · campaign updates (9.5) · counting money correctly (9.6).

9. Make it hold up

9.1 Retrieval: 5 million ads to 1,000 speed

ApproachWhy
✗ BadCheck every ad's targeting against the request5 M rule checks per request: far too slow.
~ GoodA database query on targeting columnsCorrect, but a database round trip with complex boolean conditions at 500,000 a second.
✓ GreatAn in-memory inverted index on every ad server: attribute value → ads that target it (and ads that target "anyone"), intersected for the request's attributes; plus a learned retrieval step (user and ad embeddings, nearest neighbours) for interest-based adsBoolean targeting becomes posting-list intersections in microseconds (the same structure as a search engine, Module 12). Embedding retrieval finds relevant ads that rules alone wouldn't.

9.2 The prediction budget speed · quality

9.3 Budget pacing money

Pacing has two parts. A plan: how much the campaign should have spent by now, following the day's traffic curve. A throttle: if spend is ahead of plan, the campaign takes part in fewer auctions (a participation probability), or bids lower; if behind, more. The pacing service recomputes each campaign's throttle every few seconds from reported spend and pushes it to the ad servers.

Overspend comes from delay: impressions already decided but not yet reported. Each ad server spends from a small local allocation of the remaining budget and asks for more as it runs low, so the total in flight is bounded. Near the end of a budget, allocations shrink. Clicks can arrive minutes after the impression, so CPC campaigns keep a reserve for clicks still to come.

9.4 Frequency caps speed · correctness

9.5 Campaign changes in under a minute freshness

The advertiser system publishes change events to a stream. Every ad server applies them to its in-memory index (adding and removing ad ids from posting lists) and, every hour or so, loads a full fresh snapshot to correct any drift. A pause is the most urgent change: it also goes straight to the pacing service, which sets the campaign's throttle to zero, a much smaller and faster update than an index change.

9.6 Counting money correctly money

9.7 The whole design after the deep dives

10. Interviewer follow-ups

The model-serving fleet is slow.

The ad server has a deadline per stage. If heavy predictions don't come back in time, rank by the light model's scores (or cached pCTR per ad) rather than missing the slot. Watch revenue per request to catch silent degradation.

A new ad has no click history.

The model predicts from the ad's features and its advertiser's history, and a small exploration budget shows new ads a little more often until there's data. Without that, new ads would never win and never learn.

An advertiser says they were charged for 10% more clicks than their site saw.

Compare by auction_id: some clicks are filtered as invalid and not billed; others never reach their site (users go back before the page loads). The auction and click logs make the dispute answerable.

How do you show ads to logged-out users?

Contextual targeting: the page, the search query, country and device. Interest targeting needs consent and an identity, so it's off for them.

Two ads from the same advertiser in one page?

The page asks for several slots in one request; the auction picks a set with rules (at most one per advertiser, spacing), not independent winners.

Ten times the traffic.

Ad servers and model servers scale out; the index is copied to each. Pacing state grows with campaigns, not traffic. The model fleet is the cost to watch: reduce candidates per request before adding hardware.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Model fleet slowLess relevant adsStage timeouts, revenue per requestFall back to light model or cached pCTR
Pacing service downNothing visible; spend driftsAllocation refresh errorsAd servers stop after their local allocation is used
Frequency store slowFewer capped ads shownRead latencySkip capped campaigns within the deadline
Change stream behindPaused ads still showingEvent lag per serverPause also goes via pacing (throttle 0)
Bad model versionRevenue or click rate dropsMetrics per model versionCanary rollout; fast rollback
Whole ad system downHouse ads or empty slotsError rateThe app never waits: fill with its own content

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: latency by stage · fill rate (requests that got an ad) · revenue per thousand requests · overspend per campaign · time from pause to zero spend · model calibration (predicted vs actual click rate).

At 10×: stateless parts scale out; the model fleet dominates cost, so candidate counts and model size are tuned against revenue.

A 60-minute interview

0–5Frame: pricing models, scale, deadline, budgets
5–10Must do, how well, numbers
10–15Nouns and contract
15–25First version: request path and auction
25–50Deep dives: retrieval (9.1), prediction budget (9.2), pacing (9.3)
50–60Frequency caps or money, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including auction type
8–15Must do, how well, numbers
15–22Nouns, contract
22–35First working version and the auction
35–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: cold start, disputes, slow models
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelTargeting filter, rank by bid × click rate, record impressions and clicks, a budget check.
SeniorInverted-index retrieval, a model funnel inside a deadline, eCPM and auction pricing, pacing with bounded overspend, frequency caps, auction ids for billing.
Staff+Auction design and its incentives, exploration for new ads, calibration, real-time vs billed spend, graceful degradation that protects revenue.

Common mistakes:

13. Try it, check yourself, read more

The auction lab. Three advertisers want a slot in the feed. Change their bids and see who wins and what they pay.

Garden shop: bid per click
Bike brand: bid per click
Bank: bid per click
AdvertiserBidpCTReCPM

Predicted click rates are fixed for this user and slot: garden shop 1.0%, bike brand 5.0%, bank 0.6%. Second price: the winner pays the lowest bid that would still have won, plus £0.01.

Which ad should win a slot?

A high bid nobody clicks earns nothing; expected value per impression compares them fairly.

How do you find the ads whose targeting matches a request, among 5 million?

Posting-list intersection is microseconds; the model only sees the survivors.

How do you stop overspending without a hot central counter?

Bounded money in flight, no per-impression coordination.

When should a frequency-cap counter be incremented?

Many decisions never render; counting them would cap ads people never saw.
ResourceWhy
Ad click prediction: a view from the trenches (Google, KDD 2013)Click prediction at scale, calibration and the systems around it.
Practical lessons from predicting clicks on ads at Facebook (ADKDD 2014)Features, freshness and model choice for ad ranking.
Generalized second-price auctionThe auction family used for many search and feed ads, and its incentives.