Fight it like the real interview: each phase runs against its time budget, you write your answer in a scratchpad first, then reveal the model answer side by side and grade yourself.
The Final Interview
Ten weeks. Fifty lessons. Thousands of concepts. One final test.
You've learned to think like a senior engineer. Now prove it.
The Interview Begins#
You walk into the interview room for the final round. The whiteboard is blank. The interviewer smiles — this is the principal engineer, someone who's built systems at massive scale.
"Welcome. I've heard great things about your preparation. Today, we're going to design something ambitious — a system that will test everything you know about distributed systems, reliability, and operational excellence."
They turn to the whiteboard and write:
You're building the ad-tech infrastructure for a company that serves 10 billion ad requests per day across 6 continents.
When a user loads a webpage, your system has 100ms to:
- Receive the bid request
- Query multiple demand-side platforms (DSPs)
- Run an auction
- Return the winning ad
Requirements
- 100ms end-to-end latency (p99)
- 99.99% availability (52 minutes downtime/year)
- Support 500,000 requests per second at peak
- Real-time budget tracking (can't overspend)
- Fraud detection (block invalid traffic)
- Complete audit trail for billing
"This is one of the most demanding system design problems in our industry. Take your time. Think out loud. I'm interested in your approach as much as your solution."
You take a deep breath. This is it — everything you've learned comes together now.
10 billion ad requests a day must each be answered in 100 ms p99 (receive the bid request, query the DSPs, run an auction, return the winner) at 99.99% availability, 500,000 requests a second at peak and never more than 0.1% over an advertiser's budget: about 116,000 requests a second on average, 80 TB of audit data a day, ~200 PB over 7 years and 150 servers. The 100 ms splits into 2 + 5 + 75 + 10 + 3 ms with a hard 70 ms DSP timeout, behind GeoDNS, an edge rate limiter and fraud filter, and an auction that reserves budget. 1,000 simultaneous $20 bids against $10 of budget would overspend to $29,990, so budget is a hierarchy of regional allocations and $100 pod leases; the audit trail uses a transactional outbox into Kafka, fraud detection is tiered, the Super Bowl is pre-scaled to 3x, and budget accuracy is the biggest risk.
Phase 1: Requirements Clarification#
Before touching the whiteboard, you pause. Week 1 taught you: never assume.
"Before I dive in, I'd like to clarify some requirements. This will help me make the right trade-offs."
"Of course. Good instinct."
Your Questions#
"First, let me understand the traffic pattern. You mentioned 10 billion requests per day and 500K peak RPS. What's the average RPS, and how bursty is the traffic?"
"Average is about 120,000 RPS. Peak happens during major events — Super Bowl, World Cup — where we might see about 4x normal traffic for a few hours (that is the 500,000 peak on a 120,000 average). There's also daily seasonality: traffic is 2x higher during evening hours in each timezone."
"For the 100ms latency requirement — is that from when we receive the request to when we return the response? And what happens if we can't meet it?"
"Yes, end-to-end. If you miss the deadline, the ad exchange moves on without you — you lose that impression. Speed is revenue."
"How many DSPs do we need to query for each request?"
"Typically 5-10 DSPs per request, depending on the ad slot. Each DSP has their own latency characteristics — some respond in 20ms, some take 80ms."
"For budget tracking, what's the tolerance for overspend?"
"Advertisers set daily budgets. We absolutely cannot exceed budget by more than 0.1%. At our scale, even small percentage errors mean real money."
"What about geographic distribution of traffic?"
"Roughly: 40% Americas, 35% Europe, 20% Asia-Pacific, 5% rest of world. We need low latency everywhere."
"Last question: what data do we need for the audit trail, and how long must we retain it?"
"Every bid request, every response, every auction result. 7 years retention for compliance. This is billions of events per day."
Requirements Summary#
You turn to the whiteboard and write:
Functional Requirements:
- 1. BID REQUEST HANDLING
- Receive bid request from ad exchanges
- Parse and validate request (user context, ad slot info)
- Enrich with user data (if available, privacy-compliant)
- 2. DSP ORCHESTRATION
- Fan out to 5-10 DSPs per request
- Collect bids within timeout
- Handle partial responses gracefully
- 3. AUCTION ENGINE
- Run second-price auction (this design's choice; much of the industry, including Google Ad Manager since 2019, has moved to first-price)
- Apply business rules (frequency caps, brand safety)
- Select winning bid
- 4. BUDGET MANAGEMENT
- Track spend in real-time
- Enforce daily/campaign budgets
- Pace delivery throughout the day
- 5. FRAUD DETECTION
- Detect invalid traffic (bots, click farms)
- Block suspicious requests
- Minimize false positives (lost revenue)
- 6. AUDIT & BILLING
- Log every transaction
- Support billing reconciliation
- 7-year retention
Non-Functional Requirements:
Latency
- End-to-end p99: < 100ms
- DSP timeout: 70ms (leaves margin for auction and response)
- Internal processing: < 20ms
Throughput
- Average: 120,000 RPS
- Peak: 500,000 RPS
- Must handle spikes of about 4x the average (the 500,000 peak), and be stress-tested at 5x (about 600,000)
Availability
- 99.99% uptime
- 52 minutes/year max downtime
- No single points of failure
Consistency
- Budget: Strong consistency (can't overspend)
- Auction: Eventual consistency OK
- Audit: Durable, ordered
Data scale
- 10 billion events/day
- 7 years retention
- ~200 PB raw over 7 years (~29 PB/year, before compression and tiering)
"Excellent requirements gathering. You've identified the key constraints. Now show me the architecture."
Before the numbers: requests and bytes
The interviewer gave you 10 billion requests a day, and each one leaves about 8 KB of audit data (request, DSP responses, auction result). What's the average request rate? How much audit data lands per day, per year, and over the 7 years you must keep it?
One strong answer
- Rate: 10 billion ÷ 86,400 s ≈ 116,000 requests a second, close to the 120,000 the interviewer quoted; the 500,000 peak is about 4× that.
- Data: 10 billion × 8 KB = 80 TB a day, × 365 ≈ 29 PB a year, × 7 ≈ 200 PB.
- So the audit trail can't live in one hot store: it needs hot, warm and cold tiers from day one.
Phase 2: Back of the Envelope Estimation#
"Let me validate these numbers and derive some infrastructure requirements."
Traffic Calculations#
- Daily requests: 10,000,000,000
- Seconds per day: 86,400
- Average RPS: ~116,000 (matches interviewer's 120K)
- Peak multiplier: ~4.2x (500K / 120K)
- Peak RPS: ~500,000 — ok
Per-region peak (assuming 40% in Americas):
| Region | Peak |
|---|---|
| Americas | 200,000 RPS |
| Europe | 175,000 RPS |
| APAC | 100,000 RPS |
Data Volume Calculations#
- Per bid request (raw) — request metadata: ~2 KB
- Per bid request (raw) — DSP responses (10): ~5 KB
- Per bid request (raw) — auction result: ~1 KB
- Total per request: ~8 KB
- Daily storage: 10B requests × 8 KB = 80 TB/day
- Annual storage: 80 TB × 365 = 29 PB/year
- 7-year retention: ~200 PB total — need tiered storage (hot/warm/cold)
Infrastructure Estimates#
Assuming each server handles 5,000 RPS at 70% utilization:
- Peak servers needed: 500,000 / 5,000 = 100 servers
- With 50% headroom: 150 servers globally
- Per region: Americas 60 servers · Europe 50 servers · APAC 30 servers · Other 10 servers
Latency Budget#
- Request Receive: 2ms
- Parse & Validate: 5ms
- DSP Fan-out & Collect: 75ms
- Auction Engine: 10ms
- Response Send: 3ms
- Total: 95ms total — 5ms buffer for variance
DSP TIMEOUT: 70ms hard deadline inside the 75ms fan-out window (the rest is dispatch and collection overhead)
"Good. The latency budget is particularly insightful. Now, show me the system."
Requirements and estimation
Q1/3 Which piece of this platform's state did the requirements single out for strong consistency?
Answer & explanation
Answer: B
Budget is the one place a stale read costs real money: many servers approving against the same "remaining" amount overspends, and the tolerance is only 0.1%. Auctions and analytics can be eventually consistent; forcing strong consistency there would only burn latency from the 100ms budget.
Q2/3 Each bid request produces about 8 KB of audit data (request, DSP responses, auction result). At 10 billion requests per day, roughly how much raw data lands per day?
Answer & explanation
Answer: C
10 billion x 8 KB = 80 TB per day, which is about 29 PB per year and why tiered hot/warm/cold storage is mandatory. 29 PB is the yearly figure, not daily; 8 TB and 800 TB come from slipping a factor of ten.
Q3/3 Why is the DSP timeout set to 70ms rather than letting DSPs use most of the 100ms?
Answer & explanation
Answer: A
The latency budget reserves time after fan-out for the auction (10ms) and response (3ms) plus a buffer; miss the end-to-end deadline and the exchange moves on, so the impression is lost. The timeout is derived from the latency budget, not from DSP contracts or bid prices.
Phase 3: High-Level Architecture#
"Let me draw the complete architecture, then we'll dive deep into each component."
System Architecture#
%% title: Global RTB platform · global layer
flowchart TD
DNS@{ icon: "tech:route53", label: "Route53<br/>GeoDNS" } --> CF@{ icon: "tech:cloudfront", label: "CloudFront<br/>(TLS)" } --> GC["Global<br/>Config"]:::service
CF --> USE@{ icon: "sd:region", label: "US-EAST<br/>REGION" }
CF --> EUW@{ icon: "sd:region", label: "EU-WEST<br/>REGION" }
CF --> APS@{ icon: "sd:region", label: "AP-SOUTH<br/>REGION" }
class DNS,CF edge
class USE,EUW,APS core%% title: Regional architecture (each region identical)
flowchart TD
subgraph EDGE["EDGE LAYER (Network LBs)"]
N1@{ icon: "tech:elb", label: "NLB" }
N2@{ icon: "tech:elb", label: "NLB" }
N3@{ icon: "tech:elb", label: "NLB" }
RL["RATE LIMITER (token bucket/region)<br/>+ FRAUD FILTER (ML-based filtering)"]:::edge
end
N1 & N2 & N3 --> RL
subgraph BID["BIDDING LAYER"]
subgraph FLEET["BID ORCHESTRATOR FLEET"]
direction TB
O1["Orch 1"]:::core
O2["Orch 2"]:::core
O3["Orch 3"]:::core
ON["Orch N ..."]:::core
end
DSP["DSP GATEWAY<br/>Fan-out to DSPs<br/>Timeout mgmt<br/>Circuit breaker"]:::service
AUC["AUCTION ENGINE<br/>Second-price<br/>Business rules<br/>Winner select"]:::hot
end
RL --> FLEET
FLEET --> DSP
FLEET --> AUC
subgraph DATA["DATA LAYER"]
direction TB
RC@{ icon: "tech:redis", label: "Redis Cluster<br/>(cache)" }
BS@{ icon: "sd:database", label: "Budget Store<br/>(strong)" }
USS@{ icon: "sd:cache", label: "User Store<br/>(cache)" }
CS@{ icon: "sd:database", label: "Config Store<br/>(replicated)" }
end
BID --> DATA
subgraph ASYNC["ASYNC PIPELINE"]
K@{ icon: "tech:kafka", label: "Kafka<br/>Events" } --> FL@{ icon: "tech:flink", label: "Flink<br/>Streaming" } --> S3@{ icon: "tech:s3", label: "S3<br/>Parquet" } --> RS@{ icon: "tech:redshift", label: "Redshift<br/>/Presto" }
end
DATA --> ASYNC
class N1,N2,N3 edge
class RC,USS cache
class BS,CS,S3,RS store
class K queue
class FL serviceRequest Flow#
"Let me trace a single request through the system..."
%% title: Bid request lifecycle (target: 100ms) · part 1, arrival to DSP fan-out
sequenceDiagram
autonumber
participant P as Publisher webpage
participant AX as Ad exchange
participant E as Edge (GeoDNS · NLB)
participant O as Bid orchestrator
participant D as DSPs (5-10)
Note over P,E: ① REQUEST ARRIVES (0-2ms)
P->>AX: Publisher's webpage loads
AX->>E: Ad exchange sends bid request
Note over E: GeoDNS routes to nearest region<br/>NLB receives request
Note over E: ② EDGE PROCESSING (2-7ms)<br/>Rate limiter checks quota<br/>Fraud filter: rules, then ML model<br/>(pre-trained, #lt; 2ms scoring,<br/>#lt; 5ms in all: Deep Dive 4)<br/>Request validated and parsed<br/>Enriched with cached user data
E->>O: validated, enriched request
Note over O,D: ③ DSP FAN-OUT (7-82ms)
Note over O: Orchestrator selects relevant DSPs (5-10)
par Parallel requests to all DSPs
O->>D: bid request · 70ms timeout (hard deadline)
D-->>O: Collect responses as they arrive
end
Note over O,D: Circuit breakers protect against slow DSPs%% title: Bid request lifecycle (target: 100ms) · part 2, auction to async logging sequenceDiagram autonumber 6 participant AX as Ad exchange participant O as Bid orchestrator participant A as Auction participant K as Kafka Note over A: ④ AUCTION (82-92ms) O->>A: collected bids Note over A: Run second-price auction on collected bids<br/>Apply frequency caps, brand safety rules<br/>Check budget (CRITICAL: strong consistency)<br/>Reserve budget atomically<br/>Select winner A-->>O: winner Note over AX,O: ⑤ RESPONSE (92-95ms)<br/>Format winning ad response<br/>Sign response for verification O-->>AX: Send to ad exchange Note over O,K: ⑥ ASYNC LOGGING (off the critical path)<br/>The event was saved to the outbox before replying O-)K: Shipped to Kafka (non-blocking) Note over K: Event contains: request, all bids,<br/>winner, timing<br/>Downstream: billing, analytics,<br/>fraud ML training Note over AX,K: DONE: ~95ms average, ~100ms p99
A bid request spends 0–7 ms at the edge (rate limit, a fraud model under 1 ms, validation and enrichment); then all DSPs are queried in parallel under one 70 ms deadline: open circuits are skipped, bids are collected as they arrive, and a DSP still running at the deadline is cancelled and counted as a breaker failure. A second-price auction with an atomic budget reserve runs at 82–92 ms and the response leaves at about 95 ms, with Kafka logging off the path. A DSP that turns slow opens its breaker after 5 failures and gets half-open trials after 30 s (3 successes close it); under overload, low-value requests get an immediate no bid, the timeout drops to 50 ms and only the top 3 DSPs are queried.
"Good overview. Let's dive into the hard parts. Tell me about budget management — how do you guarantee we never overspend?"
High-level architecture
Q1/3 Why does the rate limiter and fraud filter sit at the edge, before the bid orchestrator?
Answer & explanation
Answer: D
Filtering at the edge stops bots before the expensive part of the path: fanning out to 5-10 DSPs and reserving advertiser budget. The fraud filter is light: rules first, then a pre-trained gradient-boosting model that scores in under 2 ms, the whole decision under 5 ms, so it needs no special hardware.
Q2/3 In the request flow, which step needs a strongly consistent, atomic operation?
Answer & explanation
Answer: B
Budget check-and-reserve is marked CRITICAL and atomic because concurrent auctions for the same advertiser must not all spend the same remaining money. User data comes from a cache and DSP selection is a local decision; slightly stale data there is harmless.
Q3/3 Why does the Kafka publish happen after the response is sent?
Answer & explanation
Answer: C
Every millisecond of the 100ms budget is revenue, so anything not needed to answer the exchange runs asynchronously. Audit data is not optional; the deep dive on the audit trail covers how capture is still guaranteed.
What breaks if every server checks the budget on its own?
An advertiser has $10 left of a $10,000 daily budget, and 1,000 bid requests for them, each worth $20, land on different servers at the same moment. Each server reads "remaining budget" and decides on its own. What happens, and how would you keep the check off the network without overspending?
One strong answer
- All 1,000 see $10 left and all approve: spend jumps to $9,990 + $20,000 = $29,990, about 200% over budget.
- Hand out budget in leases: a global coordinator splits it across regions, each region gives pods small leases with an atomic check-and-decrement, and each pod spends only its own lease, locally.
- Be conservative: with no lease, or not enough left, reject the bid and refresh in the background. Under-spending is acceptable; overspending isn't.
Phase 4: Deep Dives#
Deep Dive 1: Real-Time Budget Management#
Week 1 concepts: Partitioning, consistency. Week 5 concepts: Distributed transactions.
"Budget management is one of the hardest problems here. Let me explain why, and then the solution."
The Challenge:
Scenario
- Advertiser has $10,000 daily budget
- Current spend: $9,990
- We have 500,000 RPS globally
- 1000 requests for this advertiser arrive simultaneously
- Each bid is $20
If each server checks "is there budget?" independently
- All 1000 see "$10 remaining"
- All 1000 approve the bid
- Actual spend: $9,990 + (1000 × $20) = $29,990
- OVERSPEND: $19,990 (200% over budget!)
This is a classic distributed consistency problem.
The Solution:
We use a hierarchical budget allocation system:
%% title: Budget architecture
flowchart TD
G["GLOBAL BUDGET COORDINATOR<br/>(source of truth)<br/>Reserve: $500 for rebalancing"]:::hot
G --> USA["US-EAST<br/>REGION BUDGET ALLOCATOR<br/>Allocation: $4,000"]:::core
G --> EUA["EU-WEST<br/>REGION BUDGET ALLOCATOR<br/>Allocation: $3,500"]:::core
G --> APA["AP-SOUTH<br/>REGION BUDGET ALLOCATOR<br/>Allocation: $2,000"]:::core
USA --> USP@{ icon: "sd:server", label: "Server Pod<br/>Local: $100" }
USA --> USM["..."]:::note
EUA --> EUP@{ icon: "sd:server", label: "Server Pod<br/>Local: $100" }
EUA --> EUM["..."]:::note
APA --> APP@{ icon: "sd:server", label: "Server Pod<br/>Local: $100" }
APA --> APM["..."]:::note
class USP,EUP,APP serviceHow it works:
- Global coordinator divides daily budget across regions (based on expected traffic distribution), keeping a small reserve back ($500 of the $10,000 here) to top up whichever region runs short
- Region allocators divide among server pods (each pod gets a "budget lease")
- Pods spend locally until lease exhausted (no network call needed for each bid)
- When lease low, request more from region allocator (background refresh, not in critical path)
- If region exhausted, request rebalancing from global (shift unused budget from other regions)
"Let me show the implementation..."
# budget/distributed_budget.py
"""
Hierarchical budget management for real-time bidding.
Provides strong consistency guarantees while minimizing
latency impact on the critical bidding path.
"""
from dataclasses import dataclass
from typing import Dict, Optional, Tuple
from datetime import datetime, timedelta, timezone
import asyncio
import logging
import time
logger = logging.getLogger(__name__)
@dataclass
class BudgetLease:
"""A local budget allocation."""
advertiser_id: str
campaign_id: str
allocated_amount: float
spent_amount: float
expires_at: float # deadline on this pod's monotonic clock (time.monotonic())
@property
def remaining(self) -> float:
return self.allocated_amount - self.spent_amount
@property
def is_expired(self) -> bool:
# Monotonic clock: wall-clock jumps (NTP steps) can't extend or cut a lease
return time.monotonic() >= self.expires_at
@property
def utilization(self) -> float:
if self.allocated_amount == 0:
return 1.0
return self.spent_amount / self.allocated_amount
class LocalBudgetManager:
"""
Manages budget leases for a single server pod.
Design principles:
- Spend decisions are LOCAL (no network in hot path)
- Background refresh keeps leases topped up
- Conservative: rather reject bids than overspend
"""
def __init__(
self,
region_allocator,
pod_id: str,
refresh_threshold: float = 0.7, # Refresh at 70% utilization
lease_duration: timedelta = timedelta(seconds=25) # holder stops at 25s; the region reclaims after 30s
):
self.region = region_allocator
self.pod_id = pod_id
self.refresh_threshold = refresh_threshold
self.lease_duration = lease_duration
self.leases: Dict[str, BudgetLease] = {}
self._refresh_lock = asyncio.Lock()
self._background: set = set() # strong refs so fire-and-forget tasks aren't garbage-collected
def _spawn(self, coro) -> None:
"""Fire-and-forget, but keep a reference until the task finishes."""
task = asyncio.create_task(coro)
self._background.add(task)
task.add_done_callback(self._background.discard)
async def can_spend(
self,
advertiser_id: str,
campaign_id: str,
amount: float
) -> Tuple[bool, str]:
"""
Check if we can spend amount. MUST BE FAST (<1ms).
Returns (can_spend, reason)
"""
key = f"{advertiser_id}:{campaign_id}"
lease = self.leases.get(key)
# No lease? Try to get one (but don't block hot path)
if lease is None:
# Trigger background refresh
self._spawn(self._refresh_lease(advertiser_id, campaign_id))
return False, "no_budget_lease"
# Lease expired?
if lease.is_expired:
self._spawn(self._refresh_lease(advertiser_id, campaign_id))
return False, "lease_expired"
# Not enough remaining?
if lease.remaining < amount:
self._spawn(self._refresh_lease(advertiser_id, campaign_id))
return False, "insufficient_budget"
# Check if we need background refresh
if lease.utilization > self.refresh_threshold:
self._spawn(self._refresh_lease(advertiser_id, campaign_id))
return True, "approved"
async def record_spend(
self,
advertiser_id: str,
campaign_id: str,
amount: float
) -> bool:
"""
Record spend against local lease.
Call this AFTER winning auction, BEFORE responding.
"""
key = f"{advertiser_id}:{campaign_id}"
lease = self.leases.get(key)
if lease is None or lease.remaining < amount:
# This shouldn't happen if can_spend was called
logger.warning(f"Spend rejected: {key}, amount={amount}")
return False
lease.spent_amount += amount
# Async report to region (for reconciliation)
self._spawn(
self.region.report_spend(advertiser_id, campaign_id, amount)
)
return True
async def _refresh_lease(
self,
advertiser_id: str,
campaign_id: str
):
"""Background refresh of budget lease."""
key = f"{advertiser_id}:{campaign_id}"
async with self._refresh_lock:
try:
# Request new allocation from region
allocation = await self.region.request_allocation(
advertiser_id=advertiser_id,
campaign_id=campaign_id,
pod_id=self.pod_id,
requested_amount=self._calculate_request_amount(key)
)
if allocation.granted_amount > 0:
self.leases[key] = BudgetLease(
advertiser_id=advertiser_id,
campaign_id=campaign_id,
allocated_amount=allocation.granted_amount,
spent_amount=0,
expires_at=time.monotonic() + self.lease_duration.total_seconds()
)
except Exception as e:
logger.error(f"Failed to refresh lease {key}: {e}")
class RegionBudgetAllocator:
"""
Manages budget allocation for a region.
Coordinates between:
- Global coordinator (gets region's share)
- Local pods (distributes to pods)
"""
def __init__(
self,
global_coordinator,
region_id: str,
redis_cluster
):
self.global_coord = global_coordinator
self.region_id = region_id
self.redis = redis_cluster
async def request_allocation(
self,
advertiser_id: str,
campaign_id: str,
pod_id: str,
requested_amount: float
) -> 'AllocationResult':
"""
Allocate budget to a pod.
Uses Redis for atomic operations within region.
"""
key = f"budget:{advertiser_id}:{campaign_id}:{self.region_id}"
# Lua script for atomic check-and-decrement
lua_script = """
local available = tonumber(redis.call('GET', KEYS[1]) or '0')
local requested = tonumber(ARGV[1])
if available <= 0 then
return 0
end
local granted = math.min(available, requested)
-- INCRBYFLOAT, not DECRBY: amounts are fractional dollars
-- (or store integer cents and use DECRBY)
redis.call('INCRBYFLOAT', KEYS[1], -granted)
return tostring(granted) -- Lua numbers become integers in replies
"""
granted = await self.redis.eval(
lua_script,
1, key, requested_amount
)
granted = float(granted)
if granted == 0:
# Try to get more from global
await self._request_global_refill(advertiser_id, campaign_id)
# Retry once
granted = float(await self.redis.eval(
lua_script,
1, key, requested_amount
))
return AllocationResult(
granted_amount=granted,
advertiser_id=advertiser_id,
campaign_id=campaign_id
)"Good. What happens during a region failure? How do you prevent double-spending?"
"Great question. We use budget reservations with expiration..."
Scenario: US-EAST region goes down
- IMMEDIATE
- Region had $4,000 allocated
- That budget is "locked" (not available to other regions)
- Global coordinator detects failure (health check fails)
- AFTER 30 SECONDS (lease expiration)
- All pod leases expire, so the region can no longer spend
- Spend is reported asynchronously, so some of the region's leases may have been spent but not yet reported
- Only provably unspent budget is released: allocation − reported spend − all outstanding pod leases
- The rest stays locked
- RECONCILIATION
- When US-EAST recovers (or its replicated spend events are read)
- Reports actual spend from before failure
- Kafka events provide source of truth
- Remaining unspent budget is released; any discrepancy flagged for review
Why this is safe
- Worst case: We UNDER-spend during failure (region can't bid, some budget locked)
- Never over-spend: only budget that provably wasn't spent is handed to other regions
- Accept temporary reduced efficiency over correctness violation
Deep Dive 2: DSP Fan-Out with Timeouts#
Week 2 concepts: Timeouts, circuit breakers, failure handling.
"The DSP fan-out is where we battle latency. Let me show how we handle partial failures gracefully."
# bidding/dsp_gateway.py
"""
DSP Gateway: Manages communication with demand-side platforms.
Challenges:
- Must query 5-10 DSPs in parallel
- Each DSP has different latency characteristics
- Total budget: 70ms (must leave margin for auction)
- Partial responses are OK (bid with what we have)
"""
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Set
from datetime import datetime, timezone
import asyncio
import logging
logger = logging.getLogger(__name__)
@dataclass
class DSPConfig:
"""Configuration for a DSP."""
dsp_id: str
endpoint: str
timeout_ms: int = 70
# Circuit breaker settings
failure_threshold: int = 5 # Failures before opening
success_threshold: int = 3 # Successes before closing
half_open_timeout_s: int = 30 # Time before retrying
# Performance tracking
p99_latency_ms: float = 50
error_rate: float = 0.01
@dataclass
class DSPBid:
"""A bid from a DSP."""
dsp_id: str
bid_id: str
price: float
ad_markup: str
latency_ms: float
received_at: datetime
@dataclass
class FanOutResult:
"""Results from DSP fan-out."""
bids: List[DSPBid]
timeouts: List[str] # DSP IDs that timed out
errors: List[str] # DSP IDs that errored
circuit_open: List[str] # DSP IDs with open circuits
total_latency_ms: float
class CircuitBreaker:
"""
Circuit breaker for a single DSP.
States:
- CLOSED: Normal operation, requests flow through
- OPEN: DSP is unhealthy, fail fast (don't even try)
- HALF_OPEN: Testing if DSP recovered
"""
def __init__(self, config: DSPConfig):
self.config = config
self.state = "CLOSED"
self.failure_count = 0
self.success_count = 0
self.last_failure_time: Optional[datetime] = None
def can_execute(self) -> bool:
if self.state == "CLOSED":
return True
if self.state == "OPEN":
# Check if we should try half-open
if self.last_failure_time:
elapsed = (datetime.now(timezone.utc) - self.last_failure_time).total_seconds()
if elapsed > self.config.half_open_timeout_s:
self.state = "HALF_OPEN"
return True
return False
if self.state == "HALF_OPEN":
return True
return False
def record_success(self):
if self.state == "HALF_OPEN":
self.success_count += 1
if self.success_count >= self.config.success_threshold:
self.state = "CLOSED"
self.failure_count = 0
self.success_count = 0
else:
self.failure_count = 0
def record_failure(self):
self.failure_count += 1
self.last_failure_time = datetime.now(timezone.utc)
if self.state == "HALF_OPEN":
self.state = "OPEN"
self.success_count = 0
elif self.failure_count >= self.config.failure_threshold:
self.state = "OPEN"
class DSPGateway:
"""
Gateway for querying DSPs with timeout and failure handling.
"""
def __init__(
self,
http_client,
dsp_configs: List[DSPConfig],
global_timeout_ms: int = 70
):
self.http = http_client
self.dsps = {cfg.dsp_id: cfg for cfg in dsp_configs}
self.global_timeout_ms = global_timeout_ms
# Circuit breakers per DSP
self.breakers = {
cfg.dsp_id: CircuitBreaker(cfg)
for cfg in dsp_configs
}
# Metrics
self.metrics = DSPMetrics()
async def fan_out(
self,
request: 'BidRequest',
target_dsps: List[str]
) -> FanOutResult:
"""
Query multiple DSPs in parallel with timeout.
Key design decisions:
1. Start all requests immediately (parallel)
2. Use global timeout (not per-DSP)
3. Collect responses as they arrive
4. Return whatever we have when timeout hits
"""
start_time = datetime.now(timezone.utc)
result = FanOutResult(
bids=[],
timeouts=[],
errors=[],
circuit_open=[],
total_latency_ms=0
)
# Filter out DSPs with open circuits
active_dsps = []
for dsp_id in target_dsps:
breaker = self.breakers.get(dsp_id)
if breaker and breaker.can_execute():
active_dsps.append(dsp_id)
else:
result.circuit_open.append(dsp_id)
if not active_dsps:
return result
# Create tasks for all DSPs
tasks = {
dsp_id: asyncio.create_task(
self._query_dsp(dsp_id, request)
)
for dsp_id in active_dsps
}
# Wait with timeout, collecting results as they arrive
pending = set(tasks.values())
deadline = self.global_timeout_ms / 1000
try:
while pending:
# Calculate remaining time
elapsed = (datetime.now(timezone.utc) - start_time).total_seconds()
remaining = deadline - elapsed
if remaining <= 0:
# Timeout! Cancel remaining and break
for task in pending:
task.cancel()
break
# Wait for next completion or timeout
done, pending = await asyncio.wait(
pending,
timeout=remaining,
return_when=asyncio.FIRST_COMPLETED
)
# Process completed tasks
for task in done:
dsp_id = self._get_dsp_id_for_task(task, tasks)
try:
bid = task.result()
if bid:
result.bids.append(bid)
self.breakers[dsp_id].record_success()
except asyncio.TimeoutError:
result.timeouts.append(dsp_id)
self.breakers[dsp_id].record_failure()
except Exception as e:
result.errors.append(dsp_id)
self.breakers[dsp_id].record_failure()
logger.warning(f"DSP {dsp_id} error: {e}")
finally:
# Cancel any still pending: they missed the deadline, which
# counts as a failure for that DSP's circuit breaker
for task in pending:
task.cancel()
dsp_id = self._get_dsp_id_for_task(task, tasks)
result.timeouts.append(dsp_id)
self.breakers[dsp_id].record_failure()
result.total_latency_ms = (
datetime.now(timezone.utc) - start_time
).total_seconds() * 1000
# Log metrics
self.metrics.record_fanout(result)
return result
async def _query_dsp(
self,
dsp_id: str,
request: 'BidRequest'
) -> Optional[DSPBid]:
"""Query a single DSP."""
config = self.dsps[dsp_id]
start = datetime.now(timezone.utc)
try:
response = await self.http.post(
config.endpoint,
json=request.to_dsp_format(),
timeout=config.timeout_ms / 1000
)
latency = (datetime.now(timezone.utc) - start).total_seconds() * 1000
if response.status == 204:
# No bid
return None
bid_data = response.json()
return DSPBid(
dsp_id=dsp_id,
bid_id=bid_data['id'],
price=bid_data['price'],
ad_markup=bid_data['adm'],
latency_ms=latency,
received_at=datetime.now(timezone.utc)
)
except asyncio.TimeoutError:
raise
except Exception as e:
logger.error(f"DSP {dsp_id} request failed: {e}")
raise"What if all DSPs are slow one day? How do you prevent cascading failures?"
"We implement backpressure at the edge. If we're falling behind, we shed load gracefully..."
When system is overwhelmed:
- DETECT OVERLOAD
- Request queue depth > threshold
- p99 latency > 80ms (approaching limit)
- CPU utilization > 80%
- SHED LOAD GRACEFULLY
- Return "no bid" immediately for low-value requests
- Prioritize high-value ad slots
- Random shedding to be fair across publishers
- ADAPTIVE TIMEOUT
- If we're fast, use full 70ms
- If we're slow, reduce to 50ms (faster failures = more capacity)
- DSP PRIORITIZATION
- During overload, only query top 3 DSPs (by historical performance). A planned peak like the Super Bowl starts gentler, at the top 5 of 10, and drops to the top 3 only if the system is still overwhelmed
- Skip consistently slow DSPs
This is Week 3's backpressure pattern applied to real-time bidding!
Budget leases and DSP fan-out
Q1/3 Why do server pods spend from local budget leases instead of checking a central budget store on every bid?
Answer & explanation
Answer: A
The hierarchy (global, region, pod) keeps the per-bid check local and sub-millisecond, while the sum of leases is bounded by the real budget. Atomicity still exists, it just moved up a level: the region allocator hands out leases with an atomic Redis check-and-decrement.
Q2/3 A bid arrives for a campaign the pod has no lease for. What does
can_spenddo?Answer & explanation
Answer: C
The design is conservative: rather reject a bid than overspend, and never block the hot path; the refresh runs as a background task. Approving and reconciling later is exactly the pattern that produced the $29,990 overspend in the opening example.
Q3/3 One DSP suddenly starts taking 500ms to respond. How does the gateway behave?
Answer & explanation
Answer: D
The global deadline bounds every auction, and the per-DSP circuit breaker stops wasting work on it, with HALF_OPEN trials allowing automatic recovery. A permanent manual removal would throw away the breaker's automatic recovery once the DSP is healthy again.
Deep Dive 3: Audit Trail at Scale#
Week 3 concepts: Event streaming, transactional outbox. Week 8 concepts: Analytics pipeline.
"Every bid request must be logged for billing and compliance. That's 10 billion events per day. Let me show the pipeline."
Requirements:
- Every event captured (no loss)
- Ordered per advertiser (for billing accuracy)
- 7-year retention
- Queryable for disputes
%% title: Audit trail architecture
flowchart TD
subgraph CP["CRITICAL PATH (sync, ~5ms)"]
BO["Bid Orchestrator<br/>1. Process request<br/>2. Run auction, debit the pod's budget lease<br/>3. Insert event into OUTBOX TABLE<br/>(one local write, before replying)<br/>4. Return response"]:::core
end
subgraph AP["ASYNC PATH (background)"]
OP["Outbox Poller<br/>(Debezium CDC)"]:::service --> K@{ icon: "tech:kafka", label: "Kafka<br/>bid-events<br/>partitioned by advertiser_id" }
K --> BC@{ icon: "sd:worker", label: "Billing<br/>Consumer" } --> BDB@{ icon: "sd:database", label: "Billing<br/>Database" }
K --> AC@{ icon: "sd:worker", label: "Analytics<br/>Consumer" } --> DL@{ icon: "tech:s3", label: "S3<br/>Data Lake" } --> GL@{ icon: "tech:glacier", label: "Glacier<br/>(7 years)" }
K --> FM@{ icon: "sd:ml", label: "Fraud ML<br/>Training" } --> FS@{ icon: "sd:database", label: "Feature<br/>Store" }
end
BO -.->|"Outbox Pattern"| OP
class K queue
class BDB,DL,GL,FS store
class FM,BC,AC serviceThe budget lives in the pods' leases and the region allocators (Deep Dive 1), not in this database, so the outbox insert shares no transaction with it. The insert is what makes the event durable: one fast local write before the reply. The Kafka publish happens later, off the critical path, which is the asynchronous logging step in the request flow. If a pod crashes after debiting its lease but before the insert, the bid was never answered, so nothing is billed and the only cost is a little unspent lease: the conservative direction.
# audit/event_pipeline.py
"""
Audit event schema and processing.
Every bid event follows this schema for consistency
across billing, analytics, and compliance.
"""
from dataclasses import dataclass, asdict
from typing import List, Dict, Optional, Any
from datetime import datetime
import json
@dataclass
class BidEvent:
"""
Immutable record of a bid transaction.
This is the source of truth for:
- Billing reconciliation
- Analytics dashboards
- Fraud investigation
- Compliance audits
"""
# Identity
event_id: str # UUID, globally unique
event_type: str # "bid_request", "bid_response", "auction_result"
event_time: datetime # When this happened (event time, not processing time)
# Request context
request_id: str # Correlates all events for one request
exchange_id: str # Which ad exchange
publisher_id: str # Which publisher
# Advertiser context
advertiser_id: str # For partitioning and billing
campaign_id: str
# Auction details
bid_price: Optional[float] = None
win_price: Optional[float] = None # Second price (what they actually pay)
is_winner: bool = False
# DSP details
dsp_id: Optional[str] = None
dsp_latency_ms: Optional[float] = None
# User context (privacy-compliant)
user_id_hash: Optional[str] = None # Hashed, not raw
geo_country: Optional[str] = None
device_type: Optional[str] = None
# Technical metadata
region: str = ""
pod_id: str = ""
processing_time_ms: float = 0
def to_json(self) -> str:
"""Serialize for Kafka."""
data = asdict(self)
data['event_time'] = self.event_time.isoformat()
return json.dumps(data)
@property
def partition_key(self) -> str:
"""Kafka partition key ensures ordering per advertiser."""
return self.advertiser_id
# Kafka topic configuration
KAFKA_TOPIC_CONFIG = {
'name': 'bid-events',
'partitions': 256, # High partition count for parallelism
'replication_factor': 3, # Durability
'retention_ms': 7 * 24 * 60 * 60 * 1000, # 7 days hot
'cleanup_policy': 'delete',
# Compression settings for efficiency
'compression_type': 'zstd', # Best compression ratio
'max_message_bytes': 1048576, # 1MB max
}"7 years of data at 10 billion events/day. How do you make that queryable for billing disputes?"
"Tiered storage with intelligent partitioning..."
| Tier | Age | Storage | Characteristics | Volume |
|---|---|---|---|---|
| HOT | 0-7 days | Kafka + Elasticsearch | Fast queries for recent data · Used for real-time dashboards | ~70 billion events |
| WARM | 7-90 days | S3 Parquet + Presto | Columnar format for analytical queries · Partitioned by date + advertiser_id | ~830 billion events |
| COLD | 90 days - 7 years | S3 Glacier | Compressed archives · Queryable with Athena after a restore (slower) · Restored on-demand for disputes | ~25+ trillion events |
Query routing
- "Last 24 hours" → Elasticsearch (sub-second)
- "Last month" → Presto on S3 (seconds)
- "2 years ago" → Glacier restore + Athena (minutes-hours)
Deep Dive 4: Fraud Detection in Real-Time#
Week 9 concepts: Security, ML inference at scale.
"We need to detect and block invalid traffic in the critical path, without adding latency."
The challenge:
- Must decide in <5ms (part of edge processing budget)
- 500K RPS of inference
- Accuracy matters (false positives = lost revenue)
SOLUTION: Tiered Detection
Tier 1: Rule-based blocking (<0.5ms)
- Known bad IPs (blocklist)
- Known data center ranges
- Rate limiting per IP/user
- Impossible geography (user in 2 countries in 1 second)
Blocks: ~5% of traffic (obvious bots)
Tier 2: ML scoring (<2ms)
- Pre-computed features (cached in Redis)
- Lightweight model (gradient boosting)
- Returns fraud probability (0-1)
| Confidence | Action |
|---|---|
| High confidence fraud (>0.95) | Block |
| Medium confidence (0.5-0.95) | Flag, bid lower |
| Low confidence (<0.5) | Allow |
Blocks: ~2% of traffic
Tier 3: Async deep analysis (non-blocking)
- Complex neural network
- Analyzes patterns over time
- Updates blocklists and ML model features
- Runs on event stream (not real-time path)
Identifies: Sophisticated attacks, click farms, etc.
# fraud/realtime_detector.py
"""
Real-time fraud detection in the bidding pipeline.
Constraint: Must complete in <5ms including all tiers.
"""
from dataclasses import dataclass
from typing import Tuple
import numpy as np
@dataclass
class FraudSignals:
"""Features extracted for fraud detection."""
ip_address: str
user_agent: str
device_id: str
geo_country: str
request_rate_1m: int # Requests from this IP in last minute
request_rate_1h: int # Requests from this IP in last hour
user_age_days: int # How long we've seen this user
session_duration: float # Current session length
click_rate: float # Historical click-through rate
class RealTimeFraudDetector:
"""
Tiered fraud detection for real-time bidding.
"""
def __init__(
self,
blocklist_store,
feature_store,
ml_model,
rate_limiter
):
self.blocklist = blocklist_store # Redis set
self.features = feature_store # Redis hash
self.model = ml_model # Pre-loaded XGBoost
self.limiter = rate_limiter
async def evaluate(
self,
signals: FraudSignals
) -> Tuple[str, float]:
"""
Evaluate fraud risk.
Returns: (decision, confidence)
- decision: "block", "flag", "allow"
- confidence: 0.0 to 1.0
"""
# TIER 1: Rule-based checks (<0.5ms)
tier1_result = await self._tier1_rules(signals)
if tier1_result == "block":
return "block", 0.99
# TIER 2: ML scoring (<2ms)
fraud_score = await self._tier2_ml_score(signals)
if fraud_score > 0.95:
return "block", fraud_score
elif fraud_score > 0.5:
return "flag", fraud_score
else:
return "allow", 1 - fraud_score
async def _tier1_rules(self, signals: FraudSignals) -> str:
"""
Fast rule-based checks.
Rules are simple, but catch obvious fraud instantly.
"""
# Check IP blocklist
if await self.blocklist.contains(signals.ip_address):
return "block"
# Check rate limits
if signals.request_rate_1m > 100: # >100 requests/minute from one IP
return "block"
# Check for data center IP ranges
if self._is_datacenter_ip(signals.ip_address):
return "block"
return "allow"
async def _tier2_ml_score(self, signals: FraudSignals) -> float:
"""
ML-based fraud scoring.
Uses pre-computed features and lightweight model.
"""
# Get historical features from Redis
historical = await self.features.get(signals.device_id)
# Build feature vector
features = np.array([
signals.request_rate_1m,
signals.request_rate_1h,
signals.user_age_days,
signals.session_duration,
signals.click_rate,
historical.get('conversion_rate', 0),
historical.get('avg_session_length', 0),
historical.get('fraud_score_history', 0),
])
# Score with pre-loaded model (XGBoost inference is fast)
score = self.model.predict_proba(features.reshape(1, -1))[0][1]
return scoreAudit trail and fraud detection
Q1/3 Why does the orchestrator insert the bid event into an outbox table before replying, instead of publishing to Kafka itself?
Answer & explanation
Answer: B
The outbox insert is one fast local write before the reply; CDC (Debezium) then ships it to Kafka, so a slow or unavailable Kafka neither delays bids nor loses billable events. The response does not wait for Kafka at all; the outbox is about not losing events, not about ordering them before the reply.
Q2/3 Why is the
bid-eventstopic partitioned byadvertiser_id?Answer & explanation
Answer: D
Kafka orders messages only within a partition, and the audit requirement is ordering per advertiser for billing accuracy. Even load is a side benefit of many keys, not the reason; a very large advertiser can actually make its partition hotter.
Q3/3 The Tier 2 model scores a request at 0.7 fraud probability. What happens?
Answer & explanation
Answer: A
Only high confidence (above 0.95) blocks; the 0.5-0.95 band is flagged and bid lower, balancing fraud losses against false positives that cost revenue. Tier 3 runs asynchronously on the event stream and never sits in the real-time path.
Trade-off: what bends during a 5× spike?
The Super Bowl will bring 5× normal traffic for a few hours. List what you'd change before and during the event, and which two requirements must not bend at all, however overloaded the system gets.
One strong answer
- Before: pre-scale every region (say to 3×), warm caches, brief on-call and check runbooks. Auto-scaling alone would lag a sudden, known spike.
- During: query fewer DSPs (the top 5), tighten the DSP timeout, cut non-critical work, and shed low-value requests with an immediate "no bid".
- Never bends: the exchange's 100 ms deadline (miss it and the impression is lost) and budget accuracy (overspend is a contract breach).
Phase 5: Scaling and Edge Cases#
"Impressive coverage. Now let's stress test your design. What happens during the Super Bowl when traffic goes 5x?"
"We handle this with predictive scaling and graceful degradation..."
Super Bowl Scale Event#
Before the event (24 hours prior)
- Pre-scale all regions to 3x capacity
- Warm up caches with likely ad content
- Alert on-call team
- Verify runbooks
During the event
- Auto-scaling handles 3x → 5x if needed
- DSP fan-out reduced (query top 5 only, not 10)
- Tighter internal budgets, not looser (e.g. DSP timeout 70 → 50ms): the exchange's 100ms deadline can't be relaxed
- Non-critical features disabled (detailed logging reduced)
If we're still overwhelmed
- Load shedding kicks in
- DSP fan-out cut further, to the top 3 (Deep Dive 2's overload rule)
- Lower-value requests get "no bid" response
- High-value requests prioritized
- Status page updated if significant
Metrics to watch
- Request queue depth
- p99 latency (should stay <100ms)
- Budget accuracy (must stay correct)
- Revenue per second (business metric)
Edge Cases#
"Let me walk through critical edge cases..."
Edge case 1: DSP Goes Rogue (Returns in 500ms)
- Problem: One DSP starts responding very slowly
- Impact: Slows down all auctions that include them
- Solution:
- Circuit breaker opens after 5 consecutive timeouts
- DSP excluded from future requests for 30s
- Automatic recovery when they improve
- Alert fires for ops awareness
Edge case 2: Clock Skew Between Regions
- Problem: Different regions see different "now"
- Impact: Budget leases might overlap or gap
- Solution:
- Leases are durations timed on each server's monotonic clock, not wall-clock timestamps compared across regions
- NTP synchronization on all servers
- Lease expiration has buffer: the holder stops at 25s, the grantor reclaims after 30s, so bounded drift can't cause overlap
- Daily reconciliation catches any drift
Edge case 3: Network Partition Between Regions
- Problem: US-EAST can't talk to global coordinator
- Impact: Can't refresh budget allocations
- Solution:
- Each region operates independently during partition
- Conservative: Stop bidding when lease expires
- Partition detection via gossip protocol
- Automatic recovery when connectivity restored
Edge case 4: Flash Crash of DSP Responses
- Problem: All DSPs suddenly return no bids
- Impact: No ads to serve, revenue drops
- Detection:
- Sudden drop in bid rate (compared to baseline)
- All DSPs affected vs one DSP affected
- Response:
- If one DSP: circuit breaker handles it
- If all DSPs: alert, may be exchange issue
- Fallback: serve house ads if configured
Scaling and edge cases
Q1/3 Why pre-scale all regions to 3x capacity a day before the Super Bowl instead of relying on auto-scaling alone?
Answer & explanation
Answer: C
Predictive scaling covers the known surge up front, auto-scaling handles 3x to 5x, and load shedding remains the last line of defense. The plan still reduces fan-out to the top 5 DSPs and keeps load shedding ready, so pre-scaling replaces neither.
Q2/3 A network partition cuts US-EAST off from the global budget coordinator. What should US-EAST do?
Answer & explanation
Answer: B
The region operates independently but conservatively: existing leases are already-allocated money, and when they run out it stops rather than risk overspending. Granting itself budget or reconciling later trades a contractual overspend for revenue, the one trade this design refuses to make.
Q3/3 Bid rate drops sharply and every DSP is affected at once. What does the design say to do?
Answer & explanation
Answer: D
Circuit breakers handle a single misbehaving DSP; when all DSPs drop together, the cause is upstream, so you alert and fall back. Opening every breaker would stop querying DSPs altogether, guaranteeing zero bids without addressing the upstream cause.
Phase 6: Monitoring and Operations#
"Let me define the SLOs and observability strategy for this system..."
SLOs and SLIs#
SLO 1: Latency
- SLI: Percentage of requests completing within 100ms
- Target: 99.9% of requests < 100ms (p99.9: stricter than the p99 requirement, so we keep a margin before breaking it)
- Measurement: Histogram of response times at load balancer
- Error Budget: 0.1% of requests can exceed 100ms — per day at 10B requests: 10 million slow requests allowed
SLO 2: Availability
- SLI: Percentage of requests that receive valid response
- Target: 99.99% availability
- Measurement: (Successful responses) / (Total requests)
- Error Budget: 0.01% requests can fail — per day at 10B requests: 1 million failures allowed
SLO 3: Budget accuracy
- SLI: Percentage of campaigns within 0.1% of budget
- Target: 99.9% of campaigns never exceed budget by >0.1%
- Measurement: (Actual spend - Budget) / Budget
This is a HARD requirement (contractual obligation).
SLO 4: Bid rate
- SLI: Percentage of requests where we submit a bid
- Target: >80% bid rate (when we have budget)
- Measurement: (Bids submitted) / (Requests with budget)
Lower bid rate = lost revenue opportunity.
Key Dashboards#
Dashboard 1: Real-time bidding health
- Requests per second (by region)
- Bid rate (should be >80%)
- Latency p50/p99 (should be <100ms)
- Error rate (should be <0.01%)
- DSP response times (per DSP)
- Circuit breaker states
Dashboard 2: Business metrics
- Revenue per second
- Spend rate (vs budget)
- Win rate (bids won / bids submitted)
- Average win price
- Top advertisers by spend
Dashboard 3: Fraud overview
- Block rate (Tier 1 rules)
- Flag rate (Tier 2 ML)
- False positive estimates
- Top blocked IPs/patterns
- Model drift indicators
Dashboard 4: Infrastructure
- CPU/Memory utilization by service
- Kafka lag (should be near zero)
- Redis memory usage
- Database connection pool status
- Regional traffic distribution
SLOs for the bidding platform
Q1/3 With 10 billion requests a day and an SLO of 99.9% under 100 ms, how many slow requests does the daily error budget allow?
Answer & explanation
Answer: B
0.1% of 10,000,000,000 is 10,000,000. The availability SLO of 99.99% allows a tenth of that: 1 million failures a day.
Q2/3 Why is the budget-accuracy SLO treated as a hard requirement, unlike latency?
Answer & explanation
Answer: B
A slow request costs one bid. Spending past a campaign's budget costs real money the platform has to absorb, so it gets far less tolerance.
Q3/3 By day 20 of the month, slow and failed requests have used up the whole error budget. What does that budget tell the team to do?
Answer & explanation
Answer: C
The error budget is the agreed amount of unreliability. Once it's spent, reliability work comes before new risk; moving the target or muting alerts just hides the problem from the people the SLO protects.
Interview Conclusion#
"This has been an exceptional session. You've demonstrated deep understanding across all dimensions — from low-level latency optimization to high-level architectural decisions. A few rapid-fire questions:"
"Why not use gRPC instead of HTTP for DSP communication?"
"Good question. gRPC would give us better performance with HTTP/2 multiplexing and protobuf serialization. The trade-off is that DSPs would need to support gRPC, which many don't. For the DSPs we control, we could use gRPC internally. For external DSPs, we'd use HTTP with connection pooling and keep-alive."
"If you had to cut latency from 100ms to 50ms, where would you start?"
"I'd focus on the DSP fan-out since that's 75ms of our budget (the 70ms DSP timeout plus dispatch and collection). Options:
- Reduce DSP count from 10 to 5 (fewer bids but faster)
- Use predictive models to guess which DSPs will bid (skip unlikely ones)
- Tighter timeouts (50ms instead of 70ms)
- Edge caching of DSP bid models for popular requests"
"What's the biggest risk in this system?"
"Budget accuracy. Unlike latency or availability where degradation is bad but recoverable, budget violations are contractual breaches. If we overspend, advertisers don't pay — we eat the cost. I'd invest heavily in testing the budget system, including chaos engineering specifically for the budget path."
"Excellent. Any questions for me?"
"Yes — at your actual scale, what was the hardest problem to solve? And how do you handle the cold start problem for new advertisers with no historical data for fraud detection?"
The interviewer smiles.
"Great questions — and exactly what I'd expect from a senior candidate. Welcome to the team."
Summary: Ten Weeks of Learning Applied#
- WEEK 1: DATA AT SCALE
- Partitioning — Budget by advertiser, events by advertiser
- Replication — Multi-region for availability
- Consistency — Strong for budget, eventual for analytics
- WEEK 2: FAILURE-FIRST DESIGN
- Timeouts — 70ms DSP timeout with budget for auction
- Circuit breakers — Per-DSP failure isolation
- Idempotency — Event IDs for exactly-once processing
- WEEK 3: MESSAGING & ASYNC
- Transactional outbox — Guaranteed event capture
- Kafka — Event streaming for audit trail
- Backpressure — Load shedding during overload
- WEEK 4: CACHING
- User data caching — Redis for enrichment
- DSP response caching — Where appropriate
- Feature caching — For real-time ML inference
- WEEK 5: CONSISTENCY & COORDINATION
- Distributed budget — Hierarchical allocation
- Lease-based consistency — Local spending with global truth
- Conflict resolution — Conservative (under-spend > over-spend)
- WEEK 6: NOTIFICATION PLATFORM
- Multi-channel delivery — Applied to DSP fan-out
- Priority queuing — High-value requests first
- Dead letter handling — Failed DSP requests tracked
- WEEK 7: SEARCH SYSTEM
- Indexing — Not directly, but similar patterns for user lookup
- Query optimization — Fast feature lookup for ML
- WEEK 8: ANALYTICS PIPELINE
- Event ingestion — 10B events/day capture
- Tiered storage — Hot/warm/cold for 7-year retention
- Late arriving data — Reconciliation processes
- WEEK 9: MULTI-TENANCY & SECURITY
- Tenant isolation — Per-advertiser budget isolation
- Fraud detection — ML-based traffic validation
- Audit compliance — Complete transaction history
- WEEK 10: PRODUCTION READINESS
- SLOs — Latency, availability, budget accuracy
- Observability — Metrics, logs, traces for debugging
- Deployment — Canary with automatic rollback
- Capacity — Predictive scaling for events
- Incidents — Runbooks for common failure modes
Self-Assessment Checklist#
After studying this capstone, you should be able to:
Requirements & Estimation:
- Clarify requirements before designing
- Calculate traffic, storage, and infrastructure needs
- Create latency budgets for time-sensitive systems
Architecture:
- Design multi-region systems for global availability
- Create appropriate separation of concerns
- Handle real-time and batch processing in one system
Distributed Systems:
- Implement strong consistency where required (budget)
- Use eventual consistency appropriately (analytics)
- Design hierarchical systems for coordination at scale
Performance:
- Optimize for latency-critical paths
- Implement parallel processing with timeouts
- Use caching effectively at multiple layers
Reliability:
- Implement circuit breakers and bulkheads
- Design for graceful degradation
- Handle edge cases and failure modes
Operations:
- Define meaningful SLOs
- Design observability into the system
- Plan for capacity and scaling events
- Create runbooks for incident response
Congratulations!#
10 Weeks · 10 Capstones · 1 Complete Toolkit
You've learned to
- Design systems that scale to millions of users
- Build for failure from the start
- Make informed trade-offs between consistency and availability
- Implement production-ready patterns
- Operate systems with confidence
- Communicate designs clearly in interviews
Remember
"The goal isn't to memorize solutions — it's to build intuition for trade-offs.
When someone asks 'how would you design X?', your first instinct should be: 'What are the constraints? What can we sacrifice?'
That's senior engineering thinking."
Go build something great.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
Go deeper · reading trail
all reading →Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.
Journey of an Ad Request: The Hidden Engineering
Ayush Kumar · 2026- What
- What happens behind one ad break on JioHotstar, from ad formats to the decision engine.
- Key idea
- An ad decision engine picks an ad in milliseconds, mixing direct deals with programmatic auctions through SSPs, and inserts it server-side or client-side.
- Why here
- The capstone's ad-serving request path and latency budget, told by an Indian streaming platform.
High-Risk, High-Scale: Guaranteeing Ad Budget Precision at 1 Million Events/Second
Abhishek Bharti · 2026- What
- How Flipkart Ads deducts impressions and clicks from advertiser budgets as they happen.
- Key idea
- Spend is aggregated with event-time semantics, dedupe and watermarks; lagging means overspend, blocking means lost revenue.
- Why here
- Exactly the capstone's budget-management deep dive: stop serving before the budget runs out without double-charging.
Photon: Fault-tolerant and Scalable Joining of Continuous Data Streams
Rajagopal Ananthanarayanan, Venkatesh Basker et al. · 2013- What
- Google's system for joining ad queries with clicks in real time across datacentres.
- Key idea
- Photon joins unordered, delayed streams with no duplicates in the output at any time and exactly-once results eventually, surviving whole-datacentre outages.
- Why here
- The capstone's billing and audit path: clicks must be joined to impressions exactly once, because each one is money.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


