~learn.
?

Boss33 minIntro+60 XP

The Grand Finale.

System Design Mastery Series — The Complete Journey

6 stages4 code

Boss · W10

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.

or read the model answer below
watch it first · newThe whole lesson in a 21-minute animated lecture5 quiz checkpoints · ask the AI about any moment · captions, chapters and speed

Intro · 1 min

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.

Interviewer

"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:

Design a Global Real-Time Bidding Platform

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:

  1. Receive the bid request
  2. Query multiple demand-side platforms (DSPs)
  3. Run an auction
  4. 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
Interviewer

"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.

ExplainerThe whole capstone: a global real-time bidding platform

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 · Stage 1 of 6 · 3 min

Phase 1: Requirements Clarification#

Before touching the whiteboard, you pause. Week 1 taught you: never assume.

Transcript
You

"Before I dive in, I'd like to clarify some requirements. This will help me make the right trade-offs."

Interviewer

"Of course. Good instinct."

Your Questions#

Transcript
You

"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?"

Interviewer

"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."

You

"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?"

Interviewer

"Yes, end-to-end. If you miss the deadline, the ad exchange moves on without you — you lose that impression. Speed is revenue."

You

"How many DSPs do we need to query for each request?"

Interviewer

"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."

You

"For budget tracking, what's the tolerance for overspend?"

Interviewer

"Advertisers set daily budgets. We absolutely cannot exceed budget by more than 0.1%. At our scale, even small percentage errors mean real money."

You

"What about geographic distribution of traffic?"

Interviewer

"Roughly: 40% Americas, 35% Europe, 20% Asia-Pacific, 5% rest of world. We need low latency everywhere."

You

"Last question: what data do we need for the audit trail, and how long must we retain it?"

Interviewer

"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:

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:

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)
Interviewer

"Excellent requirements gathering. You've identified the key constraints. Now show me the architecture."

Your turn · checkpoint 1not answered

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?

Think it through, or jot it down. Then compare.

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 · Stage 2 of 6 · 1 min

Phase 2: Back of the Envelope Estimation#

You

"Let me validate these numbers and derive some infrastructure requirements."

Traffic Calculations#

WorksheetRequests per second
  • 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#

WorksheetStorage requirements
  • 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#

WorksheetCompute requirements

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#

Worksheet100ms 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)

Interviewer

"Good. The latency budget is particularly insightful. Now, show me the system."

Quick check3 questions · +5 XP each

Requirements and estimation

  1. 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.

  2. 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.

  3. 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 · Stage 3 of 6 · 3 min

Phase 3: High-Level Architecture#

You

"Let me draw the complete architecture, then we'll dive deep into each component."

System Architecture#

DiagramGlobal RTB platform · global layer
%% 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
DiagramRegional architecture (each region identical)
%% 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 service

Request Flow#

You

"Let me trace a single request through the system..."

DiagramBid request lifecycle (target: 100ms) · part 1, arrival to DSP fan-out
%% 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
DiagramBid request lifecycle (target: 100ms) · part 2, auction to async logging
%% 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
ExplainerReal-time bidding inside 100 ms

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.

Interviewer

"Good overview. Let's dive into the hard parts. Tell me about budget management — how do you guarantee we never overspend?"

Quick check3 questions · +5 XP each

High-level architecture

  1. 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.

  2. 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.

  3. 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.

Your turn · checkpoint 2not answered

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?

Think it through, or jot it down. Then compare.

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 · Stage 4 of 6 · 18 min

Phase 4: Deep Dives#

Deep Dive 1: Real-Time Budget Management#

Week 1 concepts: Partitioning, consistency. Week 5 concepts: Distributed transactions.

You

"Budget management is one of the hardest problems here. Let me explain why, and then the solution."

The Challenge:

Why budget is hard
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:

DiagramBudget architecture
%% 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 service

How it works:

  1. 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
  2. Region allocators divide among server pods (each pod gets a "budget lease")
  3. Pods spend locally until lease exhausted (no network call needed for each bid)
  4. When lease low, request more from region allocator (background refresh, not in critical path)
  5. If region exhausted, request rebalancing from global (shift unused budget from other regions)
You

"Let me show the implementation..."

pythonBudgetLease · 240 lines
# 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
        )
Transcript
Interviewer

"Good. What happens during a region failure? How do you prevent double-spending?"

You

"Great question. We use budget reservations with expiration..."

Failure handling

Scenario: US-EAST region goes down

  1. IMMEDIATE
    • Region had $4,000 allocated
    • That budget is "locked" (not available to other regions)
    • Global coordinator detects failure (health check fails)
  2. 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
  3. 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.

You

"The DSP fan-out is where we battle latency. Let me show how we handle partial failures gracefully."

pythonDSPConfig · 279 lines
# 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
Transcript
Interviewer

"What if all DSPs are slow one day? How do you prevent cascading failures?"

You

"We implement backpressure at the edge. If we're falling behind, we shed load gracefully..."

Backpressure and load shedding

When system is overwhelmed:

  1. DETECT OVERLOAD
    • Request queue depth > threshold
    • p99 latency > 80ms (approaching limit)
    • CPU utilization > 80%
  2. SHED LOAD GRACEFULLY
    • Return "no bid" immediately for low-value requests
    • Prioritize high-value ad slots
    • Random shedding to be fair across publishers
  3. ADAPTIVE TIMEOUT
    • If we're fast, use full 70ms
    • If we're slow, reduce to 50ms (faster failures = more capacity)
  4. 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!

Quick check3 questions · +5 XP each

Budget leases and DSP fan-out

  1. 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.

  2. Q2/3 A bid arrives for a campaign the pod has no lease for. What does can_spend do?

    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.

  3. 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.

You

"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
DiagramAudit trail architecture
%% 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 service

The 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.

pythonBidEvent · 83 lines
# 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
}
Transcript
Interviewer

"7 years of data at 10 billion events/day. How do you make that queryable for billing disputes?"

You

"Tiered storage with intelligent partitioning..."

Data tiering strategy
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.

You

"We need to detect and block invalid traffic in the critical path, without adding latency."

Fraud detection architecture

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.

pythonFraudSignals · 116 lines
# 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 score
Quick check3 questions · +5 XP each

Audit trail and fraud detection

  1. 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.

  2. Q2/3 Why is the bid-events topic partitioned by advertiser_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.

  3. 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.

Your turn · checkpoint 3not answered

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.

Think it through, or jot it down. Then compare.

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 · Stage 5 of 6 · 2 min

Phase 5: Scaling and Edge Cases#

Transcript
Interviewer

"Impressive coverage. Now let's stress test your design. What happens during the Super Bowl when traffic goes 5x?"

You

"We handle this with predictive scaling and graceful degradation..."

Super Bowl Scale Event#

Handling 5x traffic spike
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#

You

"Let me walk through critical edge cases..."

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
Quick check3 questions · +5 XP each

Scaling and edge cases

  1. 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.

  2. 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.

  3. 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 · Stage 6 of 6 · 2 min

Phase 6: Monitoring and Operations#

You

"Let me define the SLOs and observability strategy for this system..."

SLOs and SLIs#

Service level objectives
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#

Operational 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
Quick check3 questions · +5 XP each

SLOs for the bidding platform

  1. 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.

  2. 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.

  3. 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.

Wrap-up · 3 min

Interview Conclusion#

Transcript
Interviewer

"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:"

Interviewer

"Why not use gRPC instead of HTTP for DSP communication?"

You

"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."

Interviewer

"If you had to cut latency from 100ms to 50ms, where would you start?"

You

"I'd focus on the DSP fan-out since that's 75ms of our budget (the 70ms DSP timeout plus dispatch and collection). Options:

  1. Reduce DSP count from 10 to 5 (fewer bids but faster)
  2. Use predictive models to guess which DSPs will bid (skip unlikely ones)
  3. Tighter timeouts (50ms instead of 70ms)
  4. Edge caching of DSP bid models for popular requests"
Transcript
Interviewer

"What's the biggest risk in this system?"

You

"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."

Interviewer

"Excellent. Any questions for me?"

You

"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.

Interviewer

"Great questions — and exactly what I'd expect from a senior candidate. Welcome to the team."

Summary: Ten Weeks of Learning Applied#

Concepts applied in this design
  • 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:

Requirements & Estimation0/3 ready
  • Clarify requirements before designing
  • Calculate traffic, storage, and infrastructure needs
  • Create latency budgets for time-sensitive systems

Architecture:

Architecture0/3 ready
  • Design multi-region systems for global availability
  • Create appropriate separation of concerns
  • Handle real-time and batch processing in one system

Distributed Systems:

Distributed Systems0/3 ready
  • Implement strong consistency where required (budget)
  • Use eventual consistency appropriately (analytics)
  • Design hierarchical systems for coordination at scale

Performance:

Performance0/3 ready
  • Optimize for latency-critical paths
  • Implement parallel processing with timeouts
  • Use caching effectively at multiple layers

Reliability:

Reliability0/3 ready
  • Implement circuit breakers and bulkheads
  • Design for graceful degradation
  • Handle edge cases and failure modes

Operations:

Operations0/4 ready
  • Define meaningful SLOs
  • Design observability into the system
  • Plan for capacity and scaling events
  • Create runbooks for incident response

Congratulations!#

Weeks 1-10 complete

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.

End of Boss · The Grand Finale
Finish BossStages clear as you read them (0/6 so far). Skipping ahead doesn't count: marking complete unlocks after ~6 more min of reading.

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.

JioHotstarBlog post · ~6 min

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.
taught here: Real-time ad decisioning
FlipkartBlog post · ~6 min

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.
taught here: Real-time budget management
GooglePaper · ~40 min

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.
taught here: Audit trail at scale

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.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…