SYSTEM DESIGN #13 · INTERVIEW GUIDE
Design a Real-Time Bidding System
Every time you see a banner ad, an auction lasting under 80 milliseconds has already been won — a bid request flew from the publisher’s SSP to dozens of DSPs, bids came back, a winner was selected, and the creative was served. This Real-Time Bidding system is one of the most latency-sensitive distributed systems ever built: it must evaluate a bid request against millions of targeting segments, check budget pacing, apply frequency caps, and return a bid price in under 10ms of internal processing time. The architecture covers IVT fraud filtering, a Redis feature store for sub-millisecond segment lookups, a token-bucket budget pacer, and a second-price auction engine with win/loss notification handling. You will learn how companies like The Trade Desk and DV360 win billions of auctions per day without blowing their clients’ budgets.
OpenRTBRedisToken BucketLightGBMKafka
💡
The Gist — What Problem Are We Solving?
A lightning-fast silent auction happening 500,000 times every second
Every time a webpage loads with an ad slot, a complete auction happens: hundreds of advertisers simultaneously submit bids in under 100 milliseconds. The highest qualified bidder wins and their ad appears — all before the page finishes loading. Miss the 100ms deadline and your bid is discarded. The challenge is doing this at 500,000 auctions per second, pricing accurately, and never overspending a campaign’s daily budget.
💬Think of it as a stock exchange for ad placements — except it runs 500,000 times per second and each auction lasts less time than a single heartbeat.
These are the capabilities the system must deliver — what users and operators can actually do with it.
⚡
Bid Processing
⚡Receive OpenRTB bid requests; return valid bid in <100ms
💰
Budget Pacing
💰Real-time spend tracking; smooth daily budget across the day
🏆
Auction Logic
🏆Second-price (Vickrey) auction; floor price; frequency cap enforcement
🤖
Bid Model
🤖ML model predicting pCTR/pCVR per impression; bid = pCTR × value per click
📊
Win/Loss Tracking
📊Record auction outcomes; feed back to pacing and model retraining
⚡
Non-Functional Requirements
These define how well the system must perform — the quality attributes that separate a toy from a production system.
⏱️ Latency SLA
⏱️<100ms hard deadline; no bid returned after = bid discarded
📈 Throughput
📈500K bid requests/sec
💰 Budget Accuracy
💰Never overspend by more than 2% of daily budget
🛡️ Fraud
🛡️Pre-bid IVT filter reduces invalid traffic before bidding
🔄 Model Freshness
🔄pCTR scores refreshed every 15 minutes
📊
Key Metrics — The Numbers That Define This System
The headline numbers to know cold — and be ready to explain how each one is achieved.
🏗️
System Architecture Diagram
Full data flow from source to serving. Each layer scales independently.
Ingestion
→
→
→
DSP Bidder
User Lookup + ML Score
↓
Processing
→
→
→
Creative Delivery; Win/Loss
↓
🗺️
End-to-End User Journey
Trace a single request end-to-end — the story interviewers want you to tell fluently.
1
User loads webpage
— Publisher ad slot fires bid request to SSP; SSP broadcasts to all eligible DSPs simultaneously
2
DSP receives bid request
— Deserialise OpenRTB JSON; extract user_id, placement context, floor price; <5ms so far
3
User lookup
— Redis GET: user_id → segment vector + pCTR score; cache hit in <1ms; in-process memory if available
4
Bid calculated
— bid_price = pCTR × value_per_click × quality_multiplier; check floor price; check frequency cap
5
Budget check
— Token bucket DECR in Redis: bid_tokens -= estimated_cost; if bucket empty → no-bid
6
Bid returned
— OpenRTB bid response returned to SSP; total time <80ms; SSP runs auction among all DSP responses
🔭
High-Level Design — Component Breakdown
Core components — each with a single, well-defined responsibility. The key architectural insight: each layer scales independently, and failure in one component is isolated from the rest.
1 — SSP/Exchange
Handles responsibilities for the SSP/Exchange layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.
2 — DSP Bidder
Handles responsibilities for the DSP Bidder layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.
3 — Feature Store
Handles responsibilities for the Feature Store layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.
4 — Budget Pacer
Handles responsibilities for the Budget Pacer layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.
5 — Auction Engine
Handles responsibilities for the Auction Engine layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.
6 — Win Handler
Handles responsibilities for the Win Handler layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.
🔬
Low-Level Design — Deep Dives
Deep dives worth explaining in detail in any senior engineering interview. For each: know the data structure, the algorithm, the why, and the trade-off you made.
1 — Bid Request Handler
OpenRTB 2.6 · <5ms parse
Receives OpenRTB 2.6 JSON bid requests from SSP partners via HTTPS. Validates required fields (imp[], site/app, user), applies pre-bid IVT check (IP blocklist, UA bot pattern), and routes to the bidding pipeline. Invalid requests returned as no-bid (HTTP 204) immediately. Request deserialization uses Protocol Buffers for 40% less CPU vs JSON. Each request tagged with a deadline timestamp; any component exceeding deadline returns no-bid immediately.
def handle_bid_request(raw_bytes):
req = openrtb_pb2.BidRequest()
req.ParseFromString(raw_bytes)
if is_ivt(req): return NO_BID
deadline = time.monotonic() + 0.075 # 75ms
return pipeline.process(req, deadline)
2 — Feature Store Lookup
Redis · <8ms · Pre-computed
User and contextual features pre-computed hourly and stored in Redis hashes: user:{id} → {segment_ids[], frequency_cap_counts{}, predicted_ctr}. Site features: site:{domain} → {category, viewability_score, brand_safety_tier}. Lookup uses Redis pipelining — all feature keys fetched in a single round-trip. Cache miss (new user): use prior distribution defaults. Feature freshness: 1-hour stale acceptable for targeting; frequency caps updated in real time.
def lookup_features(user_id, site_domain):
pipe = redis.pipeline()
pipe.hgetall(f’user:{user_id}’)
pipe.hgetall(f’site:{site_domain}’)
user_feats, site_feats = pipe.execute()
return merge_features(user_feats or DEFAULTS, site_feats or {})
3 — Budget Pacer
Token Bucket · Per-campaign
Per-campaign token bucket in Redis. Bucket state: {tokens_remaining, last_refill_ts}. Refill rate = hourly_budget / 3600 tokens/sec. Lua atomic check-and-deduct: if tokens < bid_floor, return no-bid. Deduct bid_amount on win notification. Smooth pacing: if spending 15% ahead of expected linear pace, halve the deduction rate. Distributed bidder nodes share bucket state via Redis — Lua script prevents race conditions.
— Lua: atomic budget check
local budget = tonumber(redis.call(‘HGET’, key, ‘tokens’))
if budget < tonumber(ARGV[1]) then return 0 end
redis.call('HINCRBYFLOAT', key, 'tokens', -tonumber(ARGV[1]))
return 1
4 — Auction Engine
2nd Price · Bid Shading
Second-price auction: winner pays max(second_highest_bid, floor_price) + $0.01. Bid shading for first-price auctions: shade_factor = p95(historical_clearing_price) / avg(bids_submitted). Typical shade = 85-90% of true value. Win/loss notifications consumed from SSP callback webhooks — win rate and clearing price logged to ClickHouse for bid landscape analysis and future shading calibration.
def run_auction(bids, floor):
valid = [b for b in bids if b.price >= floor]
if not valid: return None
valid.sort(key=lambda b: -b.price)
winner = valid[0]
clear_price = valid[1].price if len(valid)>1 else floor
return AuctionResult(winner=winner, price=clear_price + 0.01)
5 — IVT Post-Win Classifier
LightGBM · 50 Features
Post-win IVT classification runs asynchronously — no impact on bid latency. 50 features: click-to-conversion ratio, geographic velocity (IP location change speed), session length distribution, device fingerprint stability, time-of-day patterns. LightGBM model (300 trees, depth=6) trained on IAB MRC-validated IVT labels. Score >0.8: flag for manual review. Score >0.95: automatic credit-back request to SSP. Model retrained monthly on new labelled data.
def score_impression(impression):
features = extract_features(impression)
ivt_prob = lgb_model.predict([features])[0]
if ivt_prob > 0.95:
credit_back.request(impression.id)
elif ivt_prob > 0.8:
review_queue.add(impression.id)
return ivt_prob
⚖️
Trade-offs & Decision Log
Every senior interview comes down to these decisions. Know the exact trade-off, the reasoning, and the specific numbers that justify each choice.
⚖️ <80ms Total Latency Budget Allocation
✓
Edge Bidder (co-located with SSP) ✅ Chosen
- Network RTT reduced to <5ms — co-located in same DC as SSP
- 80ms budget split: 5ms network, 10ms feature lookup, 10ms scoring, 5ms response
- Allows 60ms for complex ML model inference
- Requires deploying bidder nodes in every major exchange PoP
→
Centralised Bidder
- Single deployment — simpler operations
- Network RTT alone consumes 40-60ms for cross-region SSPs
- Only 20ms left for computation — no room for ML models
- Frequently misses auctions due to timeout
💡Decision: Edge bidder nodes co-located at SSP PoPs; centralised budget/pacing control plane synced every 1 second
⚖️ Second-Price vs First-Price Auction
✓
Second-Price (Vickrey) — Market standard
- Dominant strategy: bid true value — no gaming required
- Well-understood by all DSPs
- Predictable spend — clear price discovery
- Header bidding blurs true second price in practice
→
First-Price ✅ Industry shift
- Winner pays exactly what they bid — higher spend risk
- Requires bid shading algorithm to avoid overpaying
- More transparent — no mystery around clearing price
- Now dominant in open web (Google, Index Exchange switched 2019)
💡Decision: Support both; bid shading algorithm (95th percentile of win prices) reduces first-price overpayment to <3%
🎯Interview Questions — Answered
The exact questions interviewers ask — with production-grade answers
Q1
How is the <80ms latency SLA enforced end-to-end?
Latency budget decomposition: SSP processing (5ms) → network RTT to DSP edge node (5ms) → IVT fraud check (2ms) → feature store lookup (8ms) → budget pacer check (2ms) → bid scoring (8ms) → response serialisation + network back (5ms) = 35ms total DSP processing. 45ms remaining is buffered for SSP overhead and network jitter. Each component has a hard timeout: if the feature store takes >10ms, the bidder uses a cached feature vector. If total time exceeds 75ms, a default bid (or no-bid) is returned immediately. Latency is tracked in a percentile histogram (p50, p95, p99) per SSP partner; any partner with p95>80ms triggers an investigation.
Q2
How does the budget pacer prevent overspending in real time?
Token bucket algorithm per campaign per hour: tokens_available = hourly_budget_remaining × (seconds_remaining_in_hour / 3600). Each winning bid deducts bid_amount from the token bucket. If tokens_available < min_bid (floor), the pacer returns no-bid immediately without consulting the ML scorer. The pacer state is maintained in Redis with millisecond precision. Across distributed bidder nodes, the pacer uses a Redis Lua DECRBY script to atomically check-and-deduct the bid amount — preventing race conditions where two nodes simultaneously win two auctions that together exceed budget. Daily budget pacing uses a smoother: bids are throttled proportionally if spending is 15% ahead of the expected linear pace.
Q3
How does the system detect and filter invalid traffic (IVT)?
Two-stage IVT pipeline: (1) Pre-bid (synchronous, <2ms): IP reputation lookup against a pre-loaded blocklist (IAB Tech Lab IPBL, ~5M IPs); User-Agent bot pattern matching; invalid bid request format check. Bid requests failing pre-bid are no-bid immediately. (2) Post-win (asynchronous, no latency impact): ML classifier (LightGBM) scoring 50+ features: click-to-conversion ratio, geographic consistency, device fingerprint stability, session length. IVT probability >0.8 = flag impression for credit back; >0.95 = automatic credit back without human review. Win events flagged as IVT are credited back within 24h and excluded from attribution.
System Design Series · Every Tuesday & Thursday
Level up your system design interviews
Each post covers Gist, Functional & Non-Functional Requirements, Key Metrics, System Diagram, User Journey, HLD, LLD, and Trade-offs & FAQs.
Subscribe to never miss a post →
Previous Articles
Categories: System Design
Tags: adtech, auction, dsp, interview prep, openrtb, real-time bidding, rtb, system design
Leave a Reply