01What exactly counts as one click — the user's tap, or landing on the advertiser site?
Users click an ad and get a 302 redirect to the advertiser site — the click is recorded server-side on that hop, so ad blockers and flaky clients cannot lose it.
Free full guide
Design an ad click aggregation system that ingests millions of click events per second and serves near-real-time plus historical aggregate metrics to advertisers.
The interview rhythm stays compact, so the page can spend attention on the actual design decisions.
Do not only state requirements. Ask for them. Each card pairs the design constraint with a clarification question you can say out loud before drawing the architecture.
01What exactly counts as one click — the user's tap, or landing on the advertiser site?
Users click an ad and get a 302 redirect to the advertiser site — the click is recorded server-side on that hop, so ad blockers and flaky clients cannot lose it.
02What granularity and freshness do advertisers need?
Advertisers query click metrics over time at a minimum granularity of one minute, available within about a minute of the click.
03If the same click arrives twice — a double-tap or a retry — should it count once?
A duplicate click on the same impression counts once — every shown ad already carries a unique impression ID.
04Clicks only, or impressions and CTR too?
Clicks first. Impressions and CTR (click-through rate) come later as a known follow-up.
05Which granularities beyond one minute - hourly, daily?
Advertisers also browse hourly and daily views; one minute stays the finest granularity.
06Do budget caps need to stop ads in near real time?
Yes — an ad that hits its budget cap must stop serving within about a minute.
Out of scopeAd targeting and ad serving (which ad to show) · Cross-device click tracking · Offline marketing channel integration
01What peak click rate should we design for?
Peak 10K clicks per second with no loss. Click data is billing data, so the pipeline is built to never drop a click.
02How fast must advertiser dashboards answer?
Queries answer in under a second across arbitrary time ranges.
03How fresh do the numbers need to be?
Near-real-time: a click should be queryable within about a minute of happening.
04Do these counts feed billing directly — so over-counting is never acceptable?
No — duplicates and retries must never inflate the numbers; these are billing-grade figures.
05How exact must the numbers be — is a brief discrepancy acceptable if corrected later?
A small transient error in the first minutes is tolerable, but billing numbers must be exact and converge within a day.
Real interviews probe far more than a tidy list. These are the scope questions that separate candidates who interrogate the problem from those who recite it.
Treat every estimate as a pressure that justifies a component: cache, queue, partition, replica, worker pool, or fallback path.
Daily event volume
Peak 10K clicks/s, ~100M clicks/day100M events × ~100 B ≈ 10 GB/day raw
Raw events are cheap to keep forever in a data lake — that is what makes daily reconciliation possible.
Aggregate row count
Assume ~10M active ads × 1 row/minute10M ads × 1,440 minutes in a day (24 h × 60 min) ≈ 14B rows/day worst case — but only ads with clicks emit rows, realistically ≪ 1%
Sparse minute rows keep the OLAP store small enough for sub-second range scans.
Dedup cache size
Assume an impression ID stays valid for a ~1-hour click window10K/s × 3,600 s × ~50 B ≈ 1.8 GB
The whole dedup window fits in one Redis cluster comfortably.
Loss window if the stream processor dies
Flink checkpoints once per minute-windowcrash → replay from last checkpoint ≈ at most 1 minute recomputed
Kafka retention (days) plus checkpoints means a processor crash recomputes, never loses.
Why not query raw events
The raw click table vs one advertiser dashboard query over 30 daysthe raw table grows 3B rows/month; even an indexed per-ad slice at minute granularity scans millions of rows per query — nowhere near sub-second
Pre-aggregation is not an optimization here — it is the design.
Decision example
Ten thousand clicks a second is money flowing in: every lost click is unbilled spend, and every double-counted click is an angry advertiser.
Record the click on the redirect hop and write it to a durable stream (Kafka) before anything else. A stream processor then aggregates continuously in one-minute windows and flushes the results into an OLAP store that the dashboards query. Each shown ad carries a signed impression ID, and a Redis check drops duplicates. Once a day, a batch job re-reads the raw events from the lake and corrects the streamed numbers.
What I would NOT do: store raw clicks in a database and run GROUP BY on every dashboard query. Three billion rows per 30-day query kills it. I also would not partition the stream purely by ad ID, since one viral ad melts a single partition. The fix is simple: append a random suffix to hot ad IDs, spread them over N partitions, and strip the suffix when writing the aggregates. The whole problem comes from picking the wrong partition key.
If advertisers accept 5-minute freshness, I would drop the streaming layer entirely and run micro-batches — half the moving parts, same accuracy, just slower.
One complete picture first, then each path as its own diagram — the write path and the read path carry different traffic and justify different components.
Complete picture
Path 1
Path 2
Before optimizing, make the contract inspectable: endpoints, entities, ownership, retries, and state.
GET/ads/{ad_id}/click?impression_id=
res302 Location: advertiser_url
The redirect hop records every attempt durably, then the stream processor deduplicates by impression_id before any billing aggregate or sink write. A cache may accelerate duplicate checks, but billing correctness must survive cache loss and replay.
GET/metrics?ad_id&from&to&granularity=1m
res200 [{ minute, clicks, unique_users }]
Served from the OLAP store, never from raw events — GROUP BY over billions of rows is what this design exists to avoid.
Core entities
ClickEventimpression_id (PK) · ad_id · user_id · clicked_at
The impression ID is minted when the ad is shown and HMAC-signed, so clicks cannot be forged or replayed.
MinuteAggregatead_id · minute · clicks · unique_users
What the OLAP store serves; one row per ad per minute.
Pick one lane for the final third of the interview. Each lane gives you the topic, the interviewer question it should answer, and the failure mode to avoid.
A user double-clicks, or a retry fires. How does the count stay at one?
Every shown ad carries a signed impression ID: the dedup cache drops repeats inside the click window, and the minute upsert is idempotent on that ID — a retry can never add a second count.
Deduping on user+ad — retargeting legitimately shows the same ad to the same user again.
A single ad suddenly takes half of all clicks. Which component hits its limit first, and how do you spread that load?
Salt the hot key — split ad_id into ad_id#0..N so the load spreads across partitions, then a tiny merge step folds the partial counts back into one minute row.
Partitioning the stream by ad ID alone — one viral ad then sends every event to the same partition, creating a hot key.
Mechanism sketch
Salting spreads one hot key across partitions; a tiny merge step reassembles the true count.
Flink dies mid-window. How many clicks are lost, and how do you know?
Nothing is lost: Kafka retains the raw events, the processor restarts from its last checkpoint and recomputes at most one window, and idempotent upserts overwrite instead of double-count.
Trusting the stream as the source of truth — Kafka retention plus checkpoints means replay, not loss.
Mechanism sketch
The restarted processor reloads the last checkpoint and replays Kafka from that offset; idempotent upserts overwrite the recomputed minute instead of double-counting it.
Streaming already aggregates in real time — what does the daily batch job add?
Streamed numbers drift — late events, crashes, bugs. Once a day the batch re-reads the raw events and overwrites every minute it recomputes: the stream buys freshness, the batch guarantees the bill.
Skipping reconciliation — billing-grade accuracy cannot rest on a best-effort stream.
Mechanism sketch
Once a day the batch recomputes every minute from raw events and overwrites drifted rows — the stream buys speed, the batch guarantees the bill.
An event lands 5 minutes after its click. Which minute does it count toward, and how long do windows wait?
Count by the click's event time, not arrival time: windows wait a bounded lateness (a few minutes), and anything later is folded in by the daily reconciliation.
Ignoring event-time vs processing-time — counting by arrival time quietly shifts money between minutes.
Talk through Ad Click Aggregator out loud and get AI scoring on the explanation.