Ad Platform
Walk through a complete ad platform design, from a basic campaign CRUD service to a two-stage retrieval-scoring pipeline serving the best ad per impression in under 100ms at 10B daily impressions, with smooth budget pacing and reliable attribution.
What is an ad management and serving system?
An ad management and serving system connects advertisers with users by selecting a matching creative per impression within a stated latency budget. The engineering challenge is the selection pipeline: thousands of campaigns compete for every impression and the winner must be chosen using relevance, bid, and remaining budget without a database scan. This question tests candidate-retrieval data structures, budget pacing with distributed counters, high-throughput event ingestion, and delayed attribution.
TL;DR
Separate the low-volume campaign control plane from the high-volume serving and measurement planes. Store campaign, creative, targeting, approval, and budget definitions in a relational database, serve creatives from an object store/CDN, and push active campaign changes into a local in-memory targeting index.
For each impression, retrieve a small candidate set from user segments, filter for eligibility and budget, and rank with a cached predicted CTR and eCPM = bid Γ predicted_ctr Γ 1000. Debit a paced token bucket before serving. Emit impression, click, and conversion events to Kafka; deduplicate and aggregate asynchronously in a columnar analytics store. Attribute conversions from a windowed event log using an explicit identity and attribution policy.
Scope and assumptions
This article uses the following illustrative capacity and policy assumptions; they must be recalculated from a real traffic distribution and pricing model:
- Approximately 10 billion impressions per day, about 120,000 impression requests per second at the stated serving peak, and 50,000 active campaigns.
- Ad selection has a 100ms p99 budget, with campaign dashboards allowed up to five minutes of freshness. Budget overspend is targeted at no more than 5% for the stated design.
- Targeting segments, cached CTR predictions, and user context are supplied by adjacent services. ML training, real-time bidding, billing, moderation, and advertiser-facing identity details are out of scope for the serving design.
- Campaign and creative changes are relatively infrequent compared with impressions. Campaign state is durable in the control plane; indexes, budget counters, and rollups are derived or ephemeral and must be rebuildable.
- Impression, click, and conversion events may be retried. At-least-once delivery with explicit event IDs and downstream deduplication is the reliability model.
Functional Requirements
Core Requirements
- Advertisers can create campaigns with targeting criteria (demographics, interests, keywords), daily and total budgets, a schedule, and creative assets (image, headline, CTA, destination URL).
- When a user's feed loads, the system selects the best-matching ad(s) to show in under 100ms.
- Ad impressions and clicks are recorded and attributed to campaigns without data loss.
- Advertisers can view spend, impressions, clicks, and conversion dashboards, updated within minutes.
Below the Line (out of scope)
- ML model training for click-through-rate (CTR) prediction.
- Real-time bidding (RTB) with external demand-side platforms (DSPs).
- Billing, invoicing, and payment processing.
- Content review and brand safety moderation.
The hardest part in scope: Selecting the best ad per impression in under 100ms. With tens of thousands of active campaigns and complex targeting predicates, a naive database scan fails immediately. The selection pipeline needs a candidate retrieval stage (milliseconds, coarse) followed by a scoring stage (ranks by expected revenue per impression).
ML model training is below the line because it is a data-infrastructure problem separate from serving. The serving layer consumes a pre-trained CTR model as a black box. An extension could ship impression and click events to a feature store, run periodic training jobs, and deploy versioned model artifacts to a registry read by the scoring service.
RTB is below the line because it introduces sub-50ms latency budgets, the OpenRTB protocol, and external DSP integrations. To add RTB, the selection response from the first-party serving layer would be submitted as a floor price in an exchange auction alongside bids from external DSPs.
Billing does not change the serving path and is handled by a standalone billing service. A billing service would accumulate spend debits against a prepaid credit ledger and pause campaigns when the balance reaches zero.
Content review is below the line because moderation requires human reviewers or a specialized ML classifier and does not affect the serving hot path. To add it, new campaigns would route through a content classification service before transitioning from pending_review to active.
Non-Functional Requirements
Core Requirements
- Selection latency: Ad selection completes in under 100ms p99. The user's feed must not block on ad selection.
- Scale: 10 billion impressions per day, approximately 120,000 impression requests per second at peak. Click rate is typically 1 to 2 percent, giving 100 to 200 million clicks per day.
- Budget accuracy: Target campaign daily spend within 5% of the configured budget. The allowed overspend and reconciliation policy must be agreed with the billing owner; this is a design target for the exercise, not a universal guarantee.
- Event durability: Impressions and clicks are captured with at-least-once delivery. Kafka retains events for 7 days for replay; acceptable duplicate rate after deduplication is under 0.01%. Losing an event is worse than recording it twice.
- Reporting freshness: Campaign dashboards reflect spend and engagement within 5 minutes. Exact real-time is not required; billing reconciliation runs hourly.
- Availability: 99.99% uptime for ad selection. Missing an impression is a direct revenue loss event. The serving path needs active-active multi-region deployment.
Below the Line
- Sub-10ms ad selection (would require on-device ML and edge-cached candidate sets)
- Exactly-once event delivery end-to-end (at-least-once with idempotent deduplication is sufficient and far cheaper)
Read/write ratio: For every campaign created, tens of millions of impression requests are evaluated against it. The serving path dwarfs campaign management by a factor of 10 million or more. Optimize for serving latency and throughput; campaign CRUD is a rounding error.
Under 100ms selection latency means a full database scan over all active campaigns is never viable at 120K requests per second. Even a 1ms per-request overhead against a single database creates a bottleneck no single instance can absorb. The selection tier must operate from in-memory data structures pre-loaded from the campaign store.
Budget accuracy at 120K impressions per second requires a distributed spend counter that can be decremented atomically without a centralized lock. This directly drives the budget pacing deep dive.
30-second answer / outline
- Put campaign CRUD, approval state, creatives, targeting, and budget policy in a relational control plane.
- Synchronize approved active campaigns into an in-memory inverted index; keep images and videos on an object store/CDN.
- On selection, fetch user segments, retrieve a small candidate set, apply eligibility and budget checks, and rank by cached
eCPM. - Debit a paced per-campaign token bucket before returning a server-generated impression ID.
- Publish impression, click, and conversion events to Kafka; deduplicate, roll up, and serve dashboards from ClickHouse or an equivalent OLAP store.
- Resolve conversion identity and touchpoints asynchronously, using an explicit attribution window and model.
5-minute explanation
Start with two very different workloads. Campaign management is a low-volume transactional control plane. Ad selection is a latency-sensitive read path that must avoid scanning the campaign database. Measurement is a third, write-heavy path where durable event capture matters more than synchronous dashboard updates.
The critical serving flow is user context β candidates β eligibility/budget β score β impression ID. A local inverted index turns targeting from a database scan into a bounded lookup. A cached CTR prediction keeps scoring local. A token bucket protects the spend budget without putting a row lock in the impression path. The selection response contains the creative URL and the ID that later event calls reference.
The critical data flow is client event β Kafka β deduplication/rollups β reporting. Kafka absorbs bursts and provides replay. ClickHouse stores the raw and aggregated event data. Conversion attribution is intentionally asynchronous because it may require an identity graph and a multi-day touchpoint window.
The main trade-offs are freshness versus serving cost, exact budget enforcement versus throughput, and richer attribution versus operational complexity. The detailed design below starts with the straightforward control plane, then shows why each serving and analytics component is needed.
45-minute interview approach
This is a time-boxed plan for answering the design question, not a claim that the article should be read in 45 minutes.
- 0β5 minutes β Clarify the contract: Confirm ad formats and count per placement, targeting dimensions, pricing model, budget tolerance, attribution window, reporting freshness, and whether RTB or ML training is included.
- 5β10 minutes β Establish scale: Use the illustrative impression rate, active-campaign count, click volume, event durability, and 100ms selection target. Separate management writes from serving reads and analytics writes.
- 10β15 minutes β Define APIs and entities: Walk through campaign/ad CRUD, selection, impression/click event capture, and reporting. State the identity and impression-ID invariants.
- 15β23 minutes β Draw the serving path: Show user-profile lookup, local inverted index, candidate scoring, budget filter, token bucket, and creative CDN. Allocate the latency budget before adding more components.
- 23β30 minutes β Deep dive on budget pacing: Compare database row locks, Redis counters, and a paced token bucket. State the allowed overspend and what happens when the pacemaker or budget store is unavailable.
- 30β36 minutes β Draw event and reporting paths: Show Kafka, deduplication, Flink or equivalent rollups, ClickHouse, and the reporting API. Explain at-least-once delivery and lag handling.
- 36β41 minutes β Deep dive on attribution and data model: Cover click-through/view-through windows, identity resolution, multi-touch models, event keys, and privacy boundaries.
- 41β45 minutes β Reliability, security, trade-offs, and close: Cover active-active serving, stale indexes, campaign kill switches, advertiser authorization, monitoring, and the main alternatives. Recap the bottleneck and invite follow-ups.
Core Entities
- Campaign: A business's advertising unit: budget (daily and total), schedule, targeting criteria, and a list of associated ads.
- Ad: A single creative unit (headline, image URL, destination URL, CTA) belonging to one campaign.
- Impression: A recorded event of an ad being displayed to a user. The primary billing and reporting unit.
- Click: A recorded event of a user clicking on an ad, referencing the originating impression ID.
- Conversion: A recorded event (purchase, sign-up) after a click or impression, reported by the advertiser's pixel or S2S postback.
- User: A platform user with behavioral signals, owned by the user profile service; the targeting layer queries it on each selection request.
The primary relationship is Campaign (1) to Ad (many). Impressions reference both the Ad shown and the User who saw it. Clicks reference an Impression. Conversions reference either a Click (click-through) or an Impression (view-through). Full schema and indexing are deferred to the deep dives.
API Design
One endpoint per core functional requirement, grouped by the requirement it satisfies.
FR 1: Create a campaign
POST /campaigns
Authorization: Bearer {advertiser_token}
Body: {
name: string,
daily_budget_cents: number,
total_budget_cents: number,
start_date: "YYYY-MM-DD",
end_date: "YYYY-MM-DD",
targeting: {
age_range: [25, 45],
interests: ["travel", "photography"],
geo: { country: "US", regions: ["CA", "NY"] },
keywords: ["mirrorless camera"]
}
}
Response 201: { campaign_id: "cmp_7x9q2k", status: "pending_review" }
The campaign starts in pending_review to allow a lightweight content check before it enters the serving pool. No ads are served from a pending_review campaign.
Create an ad inside a campaign:
POST /campaigns/{campaign_id}/ads
Authorization: Bearer {advertiser_token}
Body: {
headline: string,
image_url: string,
destination_url: string,
cta_text: string,
bid_cents: number
}
Response 201: { ad_id: "ad_4f8vja" }
bid_cents is the advertiser's maximum cost-per-click. Combined with predicted CTR, it drives the eCPM score used for selection: eCPM = bid_cents * predicted_ctr * 1000.
FR 2: Ad selection (the serving endpoint)
GET /ads/select?user_id=u_abc&placement=feed&count=1
Authorization: Bearer {platform_token}
Response 200: {
ads: [{
ad_id: "ad_4f8vja",
impression_id: "imp_9z3x1q",
headline: "Capture every moment",
image_url: "https://cdn.example.com/creatives/ad_4f8vja.jpg",
destination_url: "https://advertiser.example.com/cameras",
cta_text: "Shop now"
}]
}
The impression_id is generated server-side before responding so the client can fire an impression event using this same ID without a round trip. This separates selection latency from event recording latency.
FR 3: Record impressions and clicks
POST /events/impression
Body: { impression_id, ad_id, campaign_id, user_id, timestamp, placement }
Response 200: { ok: true }
POST /events/click
Body: { impression_id, ad_id, campaign_id, user_id, timestamp }
Response 200: { ok: true }
The client fires both endpoints as non-blocking fire-and-forget calls. Clicks include impression_id so the attribution pipeline can link the click back to the impression that preceded it. Implementation is a non-blocking Kafka publish, not a synchronous DB INSERT; the event-pipeline deep dive covers the details.
FR 4: Campaign reporting
GET /campaigns/{campaign_id}/stats?start_date=...&end_date=...&granularity=day
Authorization: Bearer {advertiser_token}
Response 200: {
impressions: 4200000,
clicks: 63000,
ctr: 0.015,
spend_cents: 441000,
conversions: 840,
data: [{ date, impressions, clicks, spend_cents }]
}
Data is served from pre-aggregated rollup tables, not a live query over billions of raw event rows. Freshness is within 5 minutes per the NFR.
High-Level Design
Critical flows
Follow four flows through the design: campaign approval and index publication; impression selection and budget debit; event capture and reporting rollups; and conversion attribution. The first establishes eligible state, the second protects the user-facing latency and budget, and the last two trade synchronous simplicity for durable asynchronous processing.
The system has four distinct flows, each mapping to one functional requirement. Build the design incrementally: start with the simplest component pair, then add only what each new requirement demands.
1. Advertisers can create campaigns with targeting and creative assets
The write path is straightforward. An advertiser submits a campaign definition; the Management Service validates it, stores it in the Campaign DB, and enqueues a review job. Once approved, the campaign becomes eligible for serving.
Components:
- Management Service: Validates campaign and ad payloads, enforces budget minimums, writes to Campaign DB.
- Campaign DB: PostgreSQL. Stores campaigns, ads, targeting criteria, budget configurations, and approval states. Low write volume (hundreds per minute, not per second).
- Review Queue: A lightweight async queue. Campaign review is a human or rule-based process that runs outside the critical path.
Request walkthrough:
- Advertiser sends
POST /campaignswith targeting rules and budget. - Management Service validates the payload (budget > 0, dates are valid, targeting schema is correct).
- Management Service inserts the campaign into Campaign DB with
status = pending_review. - Management Service enqueues a review task. Returns
campaign_idto the advertiser. - Review process approves the campaign, updating
status = active. - Campaign Sync Service (see next section) picks up the active campaign and loads it into the serving layer's in-memory index.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.
Related Articles
Design an analytics platform like Google Analytics that collects billions of user events per day, processes them through a streaming and batch pipeline, and serves query results on dashboards in seconds.
Design a personalized news feed system like Facebook's or Instagram's: from a naive fan-out-on-write to a hybrid push-pull model that serves hundreds of millions of users in under 200ms.