InterviewKata
System design · Worked design D17 of 46 · Data and infrastructure · Long form

Design an ad click aggregator

Every click on an ad is money: advertisers pay per click and want to watch the numbers move in near real time. Counting sounds trivial until clicks arrive late, twice, or ten thousand a second for one viral ad. This design is the classic introduction to stream processing: event time versus arrival time, windows, watermarks and exactly-once counts. It follows the interview framework phase by phase.

1. The prompt

"Design an ad click aggregator: users click ads on websites and apps; we send them to the advertiser's page and record the click; advertisers query click counts per ad over time (per minute, per hour), with filters such as country or device."

Two very different paths meet here: a redirect that must be instant for the user, and a counting pipeline that must be right to the click because it decides invoices.

2. Frame the problem

How many clicks?

About 10,000 a second on average, 50,000 at peak.

Far too many for a database counter per click; a log and a stream processor.

How fresh must dashboards be?

Within about a minute.

Minute windows, not per-click updates to the dashboard.

Are counts used for billing?

Yes, so the final numbers must be exact.

A fast approximate path plus an exact reconciliation path.

Smallest time unit for queries?

One minute; longer ranges by hour and day.

Pre-aggregate by minute and roll up.

Which filters?

Country, device, placement.

Each dimension multiplies rows: an OLAP store and careful choices.

What counts as a valid click?

One per impression; bots and repeats filtered.

Deduplicate by a signed impression id.

3. What it must do

  1. A click redirects the user to the advertiser's landing page.
  2. Clicks are counted per ad per minute, with filters.
  3. Advertisers query any time range at minute granularity.

Not now:

4. How well it must do it

TargetWhy this number
Fast redirect: under ~50 ms added speedThe user must not wait for counting; a slow redirect loses the click.
Fresh: dashboards within ~1 minute freshnessAdvertisers watch campaigns live and adjust budgets.
Accurate in the end: no lost or double-counted clicks consistencyBilling; disputes are costly.
Scale: 50,000 clicks a second; one ad can go viral load shapeHot keys are the norm, not the exception.

5. Numbers that change the design

QuantityWorking→ so we…
Events10,000 a second × 86,400~860M clicks a day; peak 50,000 a second → so a partitioned log (Kafka) absorbs bursts
Raw storage860M × ~200 bytes~170 GB a day → so keep it cheaply in object storage for audits and recomputation
AggregatesSay 2M active ads × 1,440 minutes~2.9B rows a day at most, far fewer in practice → so an OLAP store built for time-series rollups
Query loadDashboards refreshing every few seconds→ so pre-aggregated rows make each query a small range scan
Hot adA viral ad at 5,000 clicks a secondToo much for one task → so split hot keys

6. The nouns

click event  click_id, impression_id, ad_id, event_time, received_at, country, device, ip_hash
aggregate    (ad_id, minute, country, device) → clicks           (OLAP, rolled up to hour and day)
raw log      object storage, partitioned by hour                  (kept for audits and recounts)

7. The contract

GET /click?ad=42&imp=…&sig=…   → 302 Location: https://advertiser.example/landing
GET /v1/ads/42/clicks?from=2026-10-16T12:00Z&to=…&granularity=minute&country=IN  → [{minute, count}, …]

The impression id comes from the ad server when the ad was shown, signed so it can't be forged. It makes every click identifiable, which is the basis for deduplication.

Data flow: (1) the click service validates and redirects → (2) the event goes to Kafka → (3) a stream processor counts per ad per minute by event time → (4) counts are upserted into the OLAP store → (5) raw events land in object storage → (6) a nightly recount produces the billing numbers.

8. First working version

A click service that redirects, and a database with a counter per ad per minute.

8.1 A click

Version one increments a counter row for every click, then redirects.

8.2 A dashboard query

Sum the counter rows for the ad over the time range.

Parked for later (section 9 fixes each)

  1. The user waits on the database write before being redirected.
  2. A click that arrives late is counted in the wrong minute.
  3. Retries and repeat clicks are counted twice.
  4. One viral ad's counter row becomes a lock bottleneck.
  5. No raw record to check invoices against.

9. Make it hold up

9.1 The redirect path fast redirect

The click service validates the signature, writes the event to Kafka (asynchronously, with a local buffer) and immediately returns a 302. If Kafka is briefly slow the user is still redirected; the buffer catches up. Bot filtering runs downstream on the stream, never on the user's path.

9.2 Event time, windows and watermarks accurate · fresh

A click made at 12:00:58 on a train might reach us at 12:02:30. If we count by arrival time, it lands in the wrong minute. Stream processors such as Apache Flink count by event time (when it happened) in tumbling windows (12:00–12:01, 12:01–12:02, …).

The lab in section 13 replays a real-looking stream so you can watch the watermark, the late clicks and the duplicates.

9.3 Deduplication and exactly-once counts accurate

ApproachWhy
✗ BadIncrement a counter in a database for every clickRetries double-count, a crash between "read event" and "increment" loses or repeats counts, and one hot ad's row becomes a lock bottleneck.
~ GoodStream processor with windowed counts, written to the store as idempotent upserts keyed by (ad, minute)Re-writing the same window gives the same row. But retries inside the pipeline and duplicate clicks still inflate counts.
✓ GreatDeduplicate by impression or click id (keyed state with a TTL), checkpointed state with exactly-once processing, idempotent sinks, plus a nightly batch recount from raw eventsThe stream gives fast, nearly exact numbers; the batch job over the immutable raw log gives the final billing numbers and fixes anything the stream missed.

9.4 Hot ads scale

All clicks for one ad go to one partition and one task if keyed by ad id. A viral ad then overloads one machine. Split the key: ad_id + random(0..N) spreads the ad across N tasks, each counts its share, and a second small step adds the N partial counts. Only hot ads need splitting, and they can be detected automatically from per-key rates.

9.5 Storage and queries fresh

Minute-level aggregates go to a columnar OLAP store built for time ranges and group-bys (Apache Pinot, Apache Druid and ClickHouse are common choices). Roll minutes up into hours and days for long ranges. Raw events go to object storage, partitioned by hour, for the batch job and audits. Each extra dimension multiplies rows; pre-aggregate the common ones and query raw data for rare ones.

9.6 The nightly recount accurate

A batch job reads the day's raw events, deduplicates by click id, applies the same bot rules and the final lateness policy, and writes the billing numbers. Where it disagrees with the stream's numbers, the batch result wins and the difference is logged. Running two paths (stream plus batch) is the "lambda" shape; replaying the log through the same stream job ("kappa") avoids two code paths but needs long log retention.

9.7 The whole design after the deep dives

One click through the pipeline. Press Play or drag the timeline.

10. Interviewer follow-ups

How do you choose the out-of-order bound?

From measured delay: pick a bound that covers, say, 99.9% of clicks, and let allowed lateness and the nightly recount handle the rest. Larger bounds delay every dashboard.

Unique users who clicked.

HyperLogLog sketches per window: about 1% error in a few KB, and they merge across windows and dimensions.

Click fraud.

Rules (rate per IP or device, impossible timing) on the stream, a model in batch, and clicks marked invalid rather than deleted so the billing job can exclude them.

The stream processor crashes.

It restarts from the last checkpoint (state plus Kafka offsets) and reprocesses from there; idempotent upserts make the replay harmless.

Multi-region.

Count per region near the users, then merge regional aggregates centrally; raw logs are copied for the global recount.

An advertiser disputes a bill.

Recompute from the raw log for that ad and period, with the click ids, timestamps and invalid-click reasons as evidence.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Kafka slowNothing (redirects continue)Producer buffer sizeLocal buffer, then catch-up
Stream job downDashboards stop movingConsumer lagRestart from checkpoint; nothing lost
OLAP store slowSlow dashboardsQuery latencyRollups; cache recent results
Hot adLag for that adPer-key rateKey splitting
Stream and batch disagreeSmall dashboard correctionsNightly diffBatch is the billing truth

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 45-minute interview

0–4Frame the problem (volume, freshness, billing)
4–9Must do, how well, numbers
9–12Nouns, contract, data flow
12–18First working version, parked list
18–40Deep dives: redirect (9.1), event time (9.2), exactly-once (9.3), hot ads (9.4)
40–45Follow-ups and wrap-up

A 60-minute interview

0–5Frame the problem
5–11Must do, how well, numbers
11–15Nouns, contract, data flow
15–23First working version
23–50Deep dives 9.1–9.6 and the final picture
50–60Follow-ups: out-of-order bound, fraud, disputes; wrap-up
LevelWhat a strong answer shows
Mid-levelClick service with redirect, a queue, a consumer that aggregates per minute, a database for queries.
SeniorKafka + stream processor, event time vs processing time, windows and watermarks, idempotent upserts, deduplication by impression id, hot-key splitting, an OLAP store, raw data in object storage.
Staff+Exactly-once guarantees end to end and their cost, lateness policy tied to billing rules, batch reconciliation, fraud filtering, multi-region, dimension explosion and rollups.

Common mistakes:

13. Try it, check yourself, read more

67 clicks for one ad over five minutes, replayed in the order they arrive. Each dot falls into the minute it was clicked. The teal line is the watermark (latest event time minus 10 s); a minute's column turns teal when its window fires.

counted on timelate, still countedtoo late, droppedduplicate counted againduplicate removed

For billing, which time should a click be counted under?

Event time puts each click in the right minute, however late it arrives.

What is a watermark?

When the watermark passes a window's end, the window fires.

One viral ad overloads one stream task. What do you do?

Salting the key spreads the work; a small second step combines the partial counts.

Stream and batch counts differ for an ad. Which one goes on the invoice?

The batch job sees every event, deduplicated, with the final lateness rules.
ResourceWhy
Timely stream processing (Apache Flink docs)Event time and watermarks, from the source.
Windows (Apache Flink docs)Tumbling windows, triggers and allowed lateness.
Design an ad click aggregator (Hello Interview)Another walkthrough.