•CASE STUDY

Ads Platform Data Model (OLTP and Reporting)

6 min read·1,007 words·Intermediate

Asked at

4 candidate reports between Sep 2025 and Feb 2026

How to use this case study

SDE-2 / Mid

  • Define the core entities (advertiser, campaign, line item, creative, targeting) and their relationships
  • With keys and indexes

SDE-3 / Senior

  • Separate the transactional model from the reporting model (facts and dimensions)
  • Handle slowly changing dimensions and edit history

Staff / Principal

  • Discuss ETL/ELT pipelines
  • Late data and restatements
  • Query patterns for advertiser reports
  • Scaling the warehouse

Problem RestatementProblem

Design the data model for an advertising platform (asked at Netflix several times). Advertisers create campaigns with budgets and dates. Campaigns contain line items (or ad groups) with targeting and bids, which show creatives (the actual video or image ads). The platform records impressions and clicks. Two different needs:

  • The transactional side (OLTP): the campaign manager UI creates and edits these objects, which needs correctness and history.
  • The reporting side (OLAP): advertisers and finance ask "impressions, spend and reach per campaign per day", over billions of events.

RequirementsRequirements

  • Model advertisers, campaigns, budgets, line items, creatives, targeting, and delivery events.
  • Keep an edit history (who changed the budget, and when).
  • Support reports by time, campaign, creative, device and country.
  • Handle late events and corrections.

Transactional Model (OLTP, e.g., Postgres)

CREATE TABLE advertisers (advertiser_id BIGINT PRIMARY KEY, name TEXT, billing_account_id BIGINT, status TEXT);

CREATE TABLE campaigns (
  campaign_id BIGINT PRIMARY KEY, advertiser_id BIGINT REFERENCES advertisers,
  name TEXT, objective TEXT,                         -- awareness, reach, ...
  budget_cents BIGINT, budget_type TEXT,             -- total or daily
  start_at TIMESTAMP, end_at TIMESTAMP, status TEXT, version INT
);

CREATE TABLE line_items (
  line_item_id BIGINT PRIMARY KEY, campaign_id BIGINT REFERENCES campaigns,
  bid_type TEXT, bid_cents BIGINT, pacing TEXT, frequency_cap JSONB,
  start_at TIMESTAMP, end_at TIMESTAMP, status TEXT, version INT
);

CREATE TABLE creatives (
  creative_id BIGINT PRIMARY KEY, advertiser_id BIGINT, type TEXT,  -- video, image
  asset_url TEXT, duration_sec INT, review_status TEXT
);
CREATE TABLE line_item_creatives (line_item_id BIGINT, creative_id BIGINT, weight INT,
  PRIMARY KEY (line_item_id, creative_id));

CREATE TABLE targeting_rules (
  line_item_id BIGINT, dimension TEXT,     -- country, device, genre, audience_segment
  operator TEXT,                           -- include / exclude
  values TEXT[]
);

CREATE TABLE change_log (                  -- edit history for every entity
  entity_type TEXT, entity_id BIGINT, version INT, changed_by BIGINT,
  changed_at TIMESTAMP, before JSONB, after JSONB
);

Notes:

  • Many-to-many between line items and creatives (one creative can run in several line items).
  • Targeting as rows (dimension, include/exclude, values) is flexible: new dimensions don't need schema changes.
  • Version + change_log: every edit bumps the version and writes before/after, which gives audit history and supports "undo".

Reporting Model (OLAP / Warehouse)

Use a star schema: a big fact table of events or daily aggregates, surrounded by dimension tables that describe them.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    F[("fact_ad_delivery_daily")] --- D1["dim_date"]
    F --- D2["dim_campaign - SCD2"]
    F --- D3["dim_line_item - SCD2"]
    F --- D4["dim_creative"]
    F --- D5["dim_device"]
    F --- D6["dim_geo"]
    F --- D7["dim_advertiser"]
fact_ad_delivery_daily:
  date_key, campaign_key, line_item_key, creative_key, device_key, geo_key,
  impressions, clicks, completed_views, spend_cents, unique_reach_sketch (HLL)
  • Grain (what one row means): one row per day × campaign × line item × creative × device × country. Say the grain out loud in the interview.
  • Raw event facts (fact_impression) exist too, for deep dives, but most reports read the daily aggregate.

3.1 Slowly changing dimensions (SCD)

A campaign's name or budget changes over time. For reports to show what was true at that time, use SCD Type 2: each change creates a new dimension row with valid_from / valid_to and a new surrogate key. Facts point to the key that was valid when the event happened.

Deep Dive — Why one schema cannot serve both sidesData model

Campaign management writes a few thousand rows a day and needs every one of them exactly right. Reporting reads billions and needs sums over date ranges. Trying to serve both from one model is where ad platforms usually get stuck.

Weak

Report straight off the production tables

Advertiser dashboards query the same normalised Postgres that campaign management writes to.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  ADV["Advertiser opens the dashboard"] --> Q["SUM impressions, clicks - last 90 days"]
  Q --> PG[("OLTP - campaigns, line items, events")]
  PG --> SCAN["Row store reads every column of 2B rows"]
  SCAN --> LOCK["Long-running scans block campaign writes"]
  LOCK --> FAIL["Budget update times out mid-flight"]

A row store fetches whole rows even when a query needs two columns, so a 90-day sum reads far more than it uses. And the reporting load lands on the database that must stay available for budget and bid changes — the one place where a timeout costs real money.

Good

Point reporting at a read replica

Send dashboards to a follower. Campaign writes are protected, and reports can run as long as they like.

The contention is solved and the shape is not. The replica is still a row store with the same normalised schema, so a 90-day report still scans billions of rows through joins designed for single-campaign lookups. Reports stay slow; they are just slow somewhere harmless.

Best

Two models, connected by a pipeline

Keep the normalised OLTP schema for writes, and build a star schema in a columnar warehouse for reads.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  W["Campaign management"] --> PG[("OLTP - normalised, strongly consistent")]
  PG --> EL["ELT - change capture + hourly load"]
  EV[("Impression / click events")] --> EL
  EL --> DW[("Warehouse - fact_impressions, dim_campaign, dim_creative")]
  DW --> ROLL["Pre-aggregates - daily by campaign, by creative"]
  ROLL --> DASH["Dashboards"]
  DW --> ADHOC["Ad-hoc analysis"]

A columnar store reads only the columns a query names and compresses them heavily, which is what turns a billion-row sum into a fast query rather than a scheduled job. The star schema — a fact table of events, dimension tables for campaign, line item, creative and date — is the shape reporting tools expect, so slicing by any dimension is one join rather than five.

Two decisions to state explicitly, because they are what the interviewer is checking for:

  • The warehouse is eventually consistent, typically an hour behind. That is fine for reporting and unacceptable for spend enforcement — pacing and budget caps read live counters, never the warehouse.
  • Dimensions are versioned. A campaign renamed today must not silently rewrite last month's reports, so dimension rows carry validity ranges and facts point at the version that was current when the event happened.

Pipeline (ELT)

  1. Delivery events (impressions, clicks) stream via Kafka into the data lake (raw, partitioned by hour).
  2. Hourly jobs deduplicate, join with dimensions and build fact_ad_delivery_hourly, then roll up to daily.
  3. OLTP changes flow via CDC into dimension tables (building SCD2 history).
  4. Late events: reprocess the last 3 days each run (restatement), so late data is included. Final billing numbers are locked after the restatement window.

Trade-offs & AlternativesTrade-offs

DecisionChoiceWhyAlternative
OLTP schemaNormalized with change logCorrect edits, audit trailDenormalized docs: easier reads, harder integrity
TargetingRule rows (dimension/operator/values)FlexibleColumn per dimension: rigid
ReportingStar schema, daily aggregate factsFast, simple queriesQuery raw events every time: slow, costly
HistorySCD Type 2 dimensionsReports match what was true thenOverwrite (Type 1): history lost

Common Follow-up QuestionsFollow-ups

  • "Unique reach across days?" You can't add daily uniques together. Store HyperLogLog sketches in facts and merge them for any range.
  • "Budget vs spend?" Spend comes from facts, the budget from the campaign dimension. Pacing dashboards join them.
  • "Multi-currency?" Store spend in the advertiser's billing currency plus a normalized USD column, using the day's FX rate.

Wrap-UpWrap-up

Model the transactional side as normalized tables for advertisers, campaigns (budget and dates), line items (bids, pacing, caps), creatives, a line-item-to-creative link table, and flexible targeting rules, with versions and a change log for history. Model reporting as a star schema with a clearly stated grain, daily aggregate facts with HLL reach sketches, and SCD Type 2 dimensions, fed by an ELT pipeline that restates recent days to absorb late events.

More Case Studies

Frequently Asked Questions

What is the Ads Platform Data Model (OLTP and Reporting) system design question?

Ads Platform Data Model (OLTP and Reporting) is a system design interview question asked at FAANG companies. It covers ads, databases, analytics, data pipelines and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Ads Platform Data Model (OLTP and Reporting) question?

Netflix have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Ads Platform Data Model (OLTP and Reporting) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Ads Platform Data Model (OLTP and Reporting) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →