AdClick Aggregation Interview Questions | JiQuest

add

#

AdClick Aggregation

System design deep dive · HLD

Design an ad click aggregation system: full high-level design.

Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for click ingestion and streaming aggregation, and an entity-relationship diagram for the data model - with the reasoning an interviewer expects behind every box and arrow.

345K/sPeak click ingestion
<60sAggregation lag target
1-minRollup window
Ad clickredirect ping Ingestion svcvalidate + dedup Fraud filterbot / velocity checks Counter += 1per campaign/minute Raw click logdata lake, billing dashboard reads here

1. Clarify requirements before drawing any box

Ad click aggregation lives at the intersection of two hard problems: absorbing enormous, spiky write throughput, and producing counts advertisers will trust enough to pay an invoice against - which means correctness under duplication and fraud matters as much as speed.

Functional requirements

Ingest click eventsRecord every ad click with campaign, ad, user/device, timestamp, and referrer context.
Near-real-time aggregationAdvertisers see clicks-per-campaign-per-minute in a dashboard within roughly a minute of the click.
Fraud/bot filteringClicks from bots, click farms, or rapid repeat clicks are flagged and excluded from billable counts.
Billing-grade reconciliationA batch job over raw click logs produces the authoritative count used for invoicing, independent of the real-time path.

Non-functional requirements

Very high write throughputHundreds of thousands of click events per second at peak, globally.
Exactly-once countingA retried or duplicated click event must not be double-counted, even though delivery is at-least-once.
Durability over latency for raw eventsLosing a raw click is worse than a dashboard being a minute stale.
Correctness over recencyA slightly-delayed but correct count beats an instant but wrong one, especially for billing.
Explicitly out of scope Full ad auction/bidding logic, creative rendering, and advertiser-facing self-serve campaign management UI are called out as adjacent systems rather than core requirements, so the core design stays focused on ingest, filter, and aggregate.

2. Back-of-the-envelope capacity estimation

These numbers decide whether Kafka partitioning can keep up, how much state a streaming aggregation job must hold in memory per window, and how large the raw event data lake grows.

MetricAssumptionResulting estimate
Daily click volume10 billion ad clicks/day across the network~115,000 clicks/sec average, ~345,000 clicks/sec peak (3x)
Raw click event size~250 bytes (campaign_id, ad_id, click_id, ip/device hash, ts, referrer)10B/day × 250B ≈ 2.5 TB/day raw before compression
Active campaigns~2 million concurrently active campaigns~2M distinct counters updated per 1-minute window at peak load
Fraud filter rateIndustry-typical invalid traffic rate ~15-20%~1.5-2B clicks/day flagged and excluded from billable aggregates, still retained raw for audit
Streaming state sizePer-campaign per-minute counters, ~2M active campaigns × ~100 bytes~200 MB of hot aggregation state - fits comfortably in a stream processor's in-memory/RocksDB state store
Raw log retention2.5 TB/day × 400 days (billing dispute window)~1 PB in cold object storage, compressed/columnar
Why this matters 345,000 clicks/sec is the number that rules out any design where a single service synchronously updates a shared counter in a relational database - it forces a partitioned, append-only ingestion log with aggregation done asynchronously by a stream processor.

3. High-level design (HLD)

The HLD names the major components and the one-directional data flow from a raw click to a trustworthy per-minute count, without yet committing to partition counts, region layout, or the specific stream processing engine.

Ad serversclick redirect pixel Ingestion servicevalidate + click_id dedup Kafkapartitioned by campaign_id Fraud scoringrules + ML model Stream aggregatorwindowed count/min Aggregated countersRedis + OLAP store Raw click data lakeS3, billing batch job
Stateless/stream servicesFraud/fast-path infraServing storageDurable/async

What each box owns

Ingestion service

The single entry point for click events fired by ad servers' redirect endpoints. It validates the payload, assigns/verifies a click_id used for downstream dedup, and produces the event onto Kafka keyed by campaign_id - it deliberately does no aggregation itself, so it can scale purely by adding stateless replicas.

Fraud scoring

Consumes the same click stream and tags each event valid/invalid using a mix of fast rules (click velocity per IP/device within a short window, known datacenter IP ranges, missing device fingerprint) and an offline-trained ML model score. It annotates events rather than dropping them outright, so flagged clicks remain in the raw log for audit and appeal even though they're excluded from billable aggregates.

Stream aggregator

A stateful stream processing job (Flink/Kafka Streams style) that groups valid events into 1-minute tumbling windows keyed by campaign_id, maintains a running count in local state, and emits/updates the aggregated counter store when a window closes - handling out-of-order and slightly late events via watermarking rather than assuming perfectly ordered arrival.

Aggregated counters and the raw data lake

The counters store (Redis for the current/hot minute, an OLAP columnar store like Druid/ClickHouse for historical rollups) is what advertiser dashboards read - it is never treated as billing-authoritative. The raw click data lake retains every event, valid or not, and a separate nightly batch job recomputes the definitive billed count from it, reconciling any drift from the real-time path.

4. Detailed architecture diagram

The architecture diagram answers how Kafka partitioning survives a viral campaign, how stream processor state is checkpointed, and how the real-time and batch paths stay reconcilable - the details an interviewer checks once the HLD shape is accepted.

Edge / ingestion layer Anycast edge POPsclick redirect pixel Ingestion svc ×300 podsclick_id dedup (bloom filter) Schema validation + sampling Rate limiter (per publisher) Kafka cluster 128 partitions key = campaign_id Hot-campaign guardsalted sub-keys for hugecampaigns, merged at aggregation Stream processing cluster (Flink) Fraud scoring operator Windowed aggregation operator RocksDB state + checkpointswatermark handles late eventsexactly-once via checkpoint barrier Serving tier Redis (hot minute) Druid/ClickHouse dashboards read here - never billing-authoritative Raw data lake S3, columnar (Parquet) Nightly batch job recomputes billed count, reconciles vs. real-time Billing system Invoice generation disputes resolved against raw log, not counters
DecisionChoiceReasoning
Kafka partitioning keycampaign_id, with salted sub-keys for outlier campaignsKeeps all events for one campaign ordered together for windowed aggregation, while a salt prevents a single viral campaign from overwhelming one partition.
Lambda-style dual pathReal-time stream aggregation for dashboards + nightly batch recompute for billingReal-time counts are useful but approximate under late data and evolving fraud rules; a deterministic batch recompute over immutable raw logs is what actually gets invoiced.
Exactly-once semanticsFlink checkpointing + idempotent counter updates keyed by click_idKafka only guarantees at-least-once delivery; the stream processor's checkpoint/replay model combined with idempotent state updates is what prevents double-counting on failure recovery.
Fraud filtering is tag, not dropInvalid clicks stay in the raw log, excluded only from billable aggregatesAdvertisers dispute fraud calls; the audit trail must exist to investigate and, if needed, reverse a false positive.

5. Sequence diagrams for the two critical flows

A sequence diagram is where an interviewer checks whether a candidate understands that ingestion, fraud scoring, and aggregation are decoupled asynchronous stages rather than one synchronous call chain.

5.1 Click ingestion and durable logging

Ad server Ingestion svc Dedup cache Kafka Data lake 1. GET /click?campaign_id&click_id&ts 2. SETNX click_id (24h TTL) 3. new - not a duplicate 4. produce(campaign_id, event) 5. offset ack 6. 302 redirect to advertiser landing page 7. sink connector archives raw event (async)

Step 6 - redirecting the user to the advertiser's page - happens the instant Kafka acknowledges the write, not after fraud scoring or aggregation, because the user must never notice the ad network's internal pipeline. Step 7 is drawn as async precisely because archival to the data lake can lag by seconds without affecting anything on the user-facing critical path.

5.2 Streaming fraud filter and windowed aggregation

Kafka Fraud operator Aggregation operator Redis Dashboard 1. consume click event batch 2. score: velocity + IP reputation + ML 3. tag valid/invalid, forward valid 4. assign to 1-min tumbling window 5. late event (watermark check) → still within window 6. window closes: INCR counter[campaign][minute] 7. poll latest count 8. count + "as of" timestamp

Step 5 is the crux of streaming aggregation: a watermark (the aggregator's estimate of "events older than this have all arrived") allows the window to stay open briefly for network-delayed events before closing, rather than either closing too early (undercounting) or waiting indefinitely (never emitting). Step 8's explicit "as of" timestamp is what tells the dashboard the count is a live approximation, not the final billed number.

6. Entity-relationship (ER) diagram and schema

The data model has to answer: how is a duplicate click detected without scanning history, how is a per-minute rollup queried cheaply for a dashboard, and how does the raw log stay separable from the aggregate for billing disputes.

click_events PK click_id UUID FK campaign_id BIGINT ad_id BIGINT device_hash VARCHAR clicked_at TIMESTAMP fraud_flag ENUM campaigns PK id BIGINT advertiser_id BIGINT budget,status VARCHAR start,end TIMESTAMP aggregated_counters PK campaign_id,window_start FK campaign_id BIGINT window_start TIMESTAMP valid_clicks BIGINT flagged_clicks BIGINT updated_at TIMESTAMP N1 1N one campaign has many raw click_events and many aggregated_counters rows, one per minute window

Key modeling decisions

click_id is the primary key on click_eventsMakes the raw log itself naturally idempotent - reprocessing the same event after a stream processor restart is a safe upsert, not a duplicate row.
aggregated_counters keys on (campaign_id, window_start)A dashboard query for "last hour" is a contiguous range scan of 60 rows per campaign, not an aggregate query over raw events.
valid_clicks and flagged_clicks are separate columnsKeeps the fraud-filtered billable count and the total observed traffic both queryable, which is what lets an advertiser see their invalid-traffic rate.
click_events is partitioned by day and campaign_idMatches how it's written (streaming, per-campaign) and how it's read (the nightly batch billing job scans one day at a time).
Storage choiceUse whenWatch out for
Columnar data lake (S3 + Parquet) for click_eventsVolume is enormous (billions/day) and access is mostly big sequential batch scans for billing, not point lookups.Not suited for low-latency point queries - the real-time counters store exists specifically to avoid querying this tier for dashboards.
OLAP store (Druid/ClickHouse) for aggregated_countersDashboards need fast group-by/time-range queries across millions of campaigns.Eventually consistent with the stream; must be clearly labeled as non-authoritative for billing to avoid disputes.

7. Deep dives interviewers actually probe

How do you get exactly-once counting when Kafka only guarantees at-least-once?

Two layers of defense: the ingestion service's dedup cache rejects an obviously repeated click_id before it ever reaches Kafka, and the stream processor's checkpointing (Flink-style) makes state updates transactional with the consumer offset - if the job crashes and replays from the last checkpoint, the counter update for an already-processed event is skipped because the offset commit and the state mutation are atomic. The combination converts "delivered at-least-once" into "counted exactly-once."

How does the system handle a click event that arrives 3 minutes late?

The aggregator tracks a watermark - its running estimate of the latest timestamp it can consider "complete" - and keeps a window open for a bounded grace period (e.g. 2 minutes) past its nominal end before emitting. An event later than the grace period is routed to a "late events" side output rather than silently dropped or incorrectly reopening a closed window, and the nightly batch job (which has no such time pressure) is what ultimately incorporates every late event into the authoritative billed count.

How is bot/click-farm traffic actually detected?

Layered signals: velocity rules (more than N clicks from one device/IP within a short window), known datacenter/proxy IP range lists, missing or inconsistent device fingerprints, and an offline-trained model scoring subtler patterns (unnatural click timing distributions, conversion-rate anomalies per source). Rules catch cheap, obvious fraud in real time; the ML score, refreshed periodically from labeled outcomes, catches what static rules miss - both write to the same fraud_flag rather than being two separate pipelines.

What happens when one campaign goes viral and floods a single Kafka partition?

Partitioning strictly by campaign_id caps a single campaign's throughput at whatever one partition (and its consumer) can handle. The fix used here is key salting for outlier campaigns above a throughput threshold: the campaign's events are spread across a handful of sub-keys (campaign_id + hash(click_id) % N), consumed in parallel, and the aggregation operator merges the sub-key counts back into one campaign-level total at window-close - trading a small amount of extra merge logic for horizontal scalability on hot keys.

Why keep a separate nightly batch recompute instead of trusting the stream?

The stream aggregator optimizes for low latency under an approximate watermark and a fraud model that may be revised after the fact (e.g. a fraud ring identified a day later). The batch job re-runs over immutable raw logs with the final, possibly-updated fraud rules and no time pressure, producing a deterministic, reproducible number - this is the number advertisers are actually billed against, and any real-time/batch discrepancy is itself a monitored signal for pipeline bugs.

8. Summary: what a strong answer covers

Clarified scope before designingJustified every number with a calculationSeparated real-time from billing-authoritative counts Named the partitioning key and its hot-key fixExplained watermarks for late eventsTreated fraud as tag-not-drop for auditability
Interview tip When asked to design an ad click aggregation system, the strongest signal is proposing a lambda-style dual path up front - fast approximate stream aggregation for dashboards, slower deterministic batch recompute for billing - rather than trying to make one pipeline serve both jobs, which is where most designs quietly become incorrect under load.
No comments
Leave a Comment