01Where exactly do we capture the click — on the client, or on the redirect?
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.
01Where exactly do we capture the click — on the client, or on the redirect?
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.
03What happens when the same click arrives twice?
The same impression clicked twice counts once — every shown ad carries a unique impression ID, and counting is idempotent on it.
04Clicks only, or impressions and CTR too?
Clicks day one - but every click already carries impression_id, so impressions and CTR (click-through rate) can join later without re-instrumenting the pipeline.
05Which granularities beyond one minute - hourly, daily?
Minute is the base truth; hourly and daily are rollups derived inside the OLAP store (the pre-aggregated analytics database) - a dashboard query never recomputes from raw events.
06Do budget caps need to stop ads in near real time?
Yes: budget enforcement consumes the same minute aggregates - an ad hitting its cap stops serving within about a minute, no separate counting system.
Out of scopeAd targeting and ad serving (which ad to show) · Cross-device click tracking · Offline marketing channel integration
01What peak click rate should ingestion survive?
Peak 10K clicks per second without loss — click data is billing data; the pipeline is built for zero-loss collection.
02How fast must advertiser dashboards answer?
Queries answer in under a second across arbitrary time ranges — which is what forces pre-aggregation into an OLAP store.
03How fresh do the numbers need to be?
Near-real-time: a click should be queryable within about a minute of happening.
04Can we ever double-count?
No — duplicates and retries never inflate the numbers; idempotency keys off a signed impression ID.
05Is the streaming number the final truth?
No — the stream gives speed; a daily batch job over the raw event lake re-aggregates and corrects, so accuracy is guaranteed even when the stream hiccups.
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
10M active ads × 1 row/minute10M × 1,440 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
Impression IDs 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.
I would put the click on the redirect hop and write it to a durable stream (Kafka) before anything else — then aggregate continuously in one-minute windows with a stream processor, and flush results into an OLAP store 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 per dashboard query — three billion rows per 30-day query kills it. I also would not partition the stream purely by ad ID: one viral ad melts a single partition. The fix is boring and effective — append a random suffix to hot ad IDs, spread them over N partitions, and strip the suffix when writing aggregates. Picking the wrong partition key is how you build a hot partition by hand.
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.
Draw the write path and the read path as separate lanes — they carry different traffic and justify different components.
Write to the stream before the cache — cache loss must never mean click loss. Dashed = off the real-time path. The daily batch re-reads raw events and overwrites any streamed number that drifted.
The 302 answers as soon as the event is durably in Kafka; windowed aggregation and OLAP flush happen behind it, within the one-minute freshness budget.
Every dashboard query hits pre-aggregated minute rows; raw events exist only for replay and the daily reconciliation.
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.
FocusThe same click, twice
AskA user double-clicks, or a retry fires. How does the count stay at one?
AvoidDeduping on user+ad — retargeting legitimately shows the same ad to the same user again.
FocusOne ad goes viral
AskA single ad takes half of all clicks. What melts first, and how do you spread the load?
AvoidPartitioning the stream by ad ID alone — that is building a hot partition by hand.
FocusThe stream processor crashes
AskFlink dies mid-window. How many clicks are lost, and how do you know?
AvoidTrusting the stream as the source of truth — Kafka retention plus checkpoints means replay, not loss.
FocusWhy keep a batch layer at all
AskStreaming already aggregates in real time — what does the daily batch job add?
AvoidSkipping reconciliation — billing-grade accuracy cannot rest on a best-effort stream.
FocusA click arrives late
AskAn event lands 5 minutes after its click. Which minute does it count toward, and how long do windows wait?
AvoidIgnoring 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.