System Design (High-Level Design) Module 29 – Case studies: platforms, analytics and developer tools
Design an ad click aggregator
Count ad clicks per ad and minute in event time, remove repeats and invalid traffic, and reconcile streaming totals with a daily recount for billing.
What you will learn
- Estimate click rates, raw storage and bandwidth for an ad-click pipeline
- Remove repeated deliveries of clicks by click id at high rates
- Aggregate clicks per ad and minute in event time, with late clicks handled
- Check streaming totals against a batch recount of the raw clicks for billing
Before you start
What you’ll learn · Before you start
What you will learn
- Estimate click rates, raw storage and bandwidth for an ad-click pipeline
- Remove repeated deliveries of clicks by click id at high rates
- Aggregate clicks per ad and minute in event time, with late clicks handled
- Check streaming totals against a batch recount of the raw clicks for billing
Before you start
On this page
An ad network records 864 million clicks a day, and two readers want different things from them. Advertisers watch dashboards that should change within seconds, and they pay invoices that must count every valid click exactly once.
The design that serves both keeps every raw click in an append-only log and reads that log twice. A streaming job removes repeated deliveries by click id, drops traffic that known-bot lists catch, and adds each click to the minute in which it happened, not the minute in which it arrived, so dashboards stay fresh. A daily recount reads the same raw clicks again, applies rules that need a whole day of data, and produces the numbers that are billed; any difference between the two is explained before an invoice goes out.
Five questions decide how exact each number must be
Ask what each number is used for, because that decides how exact it must be. The second column holds the answers this case study assumes:
| Question | Assumed here |
|---|---|
| Does a click pass through our own redirect, or does an app report it? | Web clicks do; apps send theirs in batches, sometimes hours late |
| What is a duplicate: one click delivered twice, or two clicks by one person? | A signed token per impression carries a click id; a second click on it counts once |
| How fresh must a dashboard be? | A minute of lag |
| How late can a click arrive? | Hours, from phones that were offline |
| Which numbers are billed, and for how long can they be disputed? | Invoices are monthly, from daily totals that can be recounted |
Functional:
- Record a click and send the person on to the advertiser’s page.
- Show clicks per ad, campaign and advertiser per minute, by country and device, for any time range.
- Produce the billed clicks of each advertiser per day, with invalid clicks removed.
- Correct published counts when late clicks or newly found invalid traffic change them.
Non-functional:
- Fast redirects: counting never slows the redirect down.
- No lost clicks: a click the service has acknowledged survives the failure of any one machine.
- Fresh dashboards: a click shows up within a minute.
- Exact bills: each valid click is billed once, and every billed number can be recounted from raw data.
The Media Rating Council’s invalid-traffic standard shapes the last requirement. It says that invalid traffic found through lists and routine checks “must be removed from monetized counts”, and that when a new kind is found, the counts of at least the 14 days before the discovery are checked again (Media Rating Council). So raw clicks must be kept long enough to be counted again.
Checkpoint
Check: What each number is used for
The estimate: 30,000 clicks a second at the peak
Assume 864 million clicks a day, the busiest hour at three times the average, 400 bytes for each raw click and 30 days of raw clicks kept for recounts:
"""The ad-click aggregator's estimates from stated assumptions: change any of them and run it again."""
DAY = 86_400 # seconds
clicks_per_day = 864_000_000 # every click recorded, valid or not, across all ads
peak = 3 # the busiest hour against the day's average
event_bytes = 400 # one raw click: click id, ad, campaign, event time, country, device, flags
raw_days = 30 # raw clicks kept for recounts, audits and new invalid-traffic checks
ads_per_minute = 400_000 # different ads that get at least one click in a busy minute
row_bytes = 40 # one (ad, minute) row: the ids, the minute, valid and invalid clicks
dashboards = 10_000 # advertiser dashboards open at the peak
refresh = 10 # seconds between two refreshes of one dashboard
per_partition = 5_000 # clicks a second that one partition of the streaming job keeps up with
avg = clicks_per_day / DAY
top = avg * peak
raw_day = clicks_per_day * event_bytes
rows_day = ads_per_minute * 1_440
print(f"Clicks: {avg:,.0f} a second on average, {top:,.0f} in the busiest hour")
print(f"Raw clicks: {raw_day / 1e9:,.1f} GB a day, {raw_day * raw_days / 1e12:,.1f} TB kept for {raw_days} days")
print(f"Arriving at the peak: {top * event_bytes / 1e6:,.1f} MB/s ({top * event_bytes * 8 / 1e6:,.0f} Mbit/s)")
print(f"Minute rows: {rows_day:,} a day, {rows_day * row_bytes / 1e9:,.1f} GB a day")
print(f"Raw clicks against minute rows: {raw_day / (rows_day * row_bytes):,.0f} times the bytes")
print(f"Dashboard queries at the peak: {dashboards / refresh:,.0f} a second")
print(f"Partitions busy at the peak: {top / per_partition:,.0f} of {per_partition:,} clicks a second each") Output
Clicks: 10,000 a second on average, 30,000 in the busiest hour Raw clicks: 345.6 GB a day, 10.4 TB kept for 30 days Arriving at the peak: 12.0 MB/s (96 Mbit/s) Minute rows: 576,000,000 a day, 23.0 GB a day Raw clicks against minute rows: 15 times the bytes Dashboard queries at the peak: 1,000 a second Partitions busy at the peak: 6 of 5,000 clicks a second each
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 estimates.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The rates are moderate: 30,000 clicks a second at the peak is 12 MB/s, which six partitions of a streaming job can take at 5,000 clicks a second each. Storage is shaped by the raw data: 345.6 GB a day of raw clicks against 23 GB of minute rows, 15 times as many bytes. Counts are what dashboards read; the raw clicks are what billing trusts.
Checkpoint
Check the estimate
A click id in every link, and three records
GET /c/{token}: the link behind every ad. The service checks the token’s signature, writes the click to the log and answers302 Found, with the advertiser’s address in theLocationheader (RFC 9110).POST /v1/app-clicks: apps send their queued clicks in batches, each with the token it was shown.GET /v1/ads/{adId}/clicks?from=…&to=…&step=minute: counts for a dashboard.GET /v1/advertisers/{id}/billing?month=…: billed clicks per ad and day, final once a day is closed.
Three records hold the data, each keyed for the path that reads it:
| Record | Key | Holds |
|---|---|---|
| Raw click | The click id, from the signed token | The ad, campaign and advertiser, the event time (when the click happened), the arrival time, the country, the device class and the result of each check |
| Minute row | An ad and a minute | Its valid and invalid clicks; rolled up into hours after a week and into days after a month |
| Billing row | An advertiser, an ad and a day | Its billed clicks and the version of the recount that produced it |
Checkpoint
Check: The click id and the three records
The high-level design, walked along one click
The ad-click aggregator's high-level design
Text description of the diagram
The diagram runs from top to bottom.
- A person clicks an ad in a browser or an app, which sends GET /c/{token} to the click service.
- The click service checks the signed token, writes the raw click to the click log and then answers with a 302 redirect to the advertiser's page (a dashed arrow back to the person).
- The click log holds raw clicks, partitioned by ad, replicated and kept for 7 days.
- A streaming job reads the log. It removes repeated deliveries by click id, drops clicks that bot lists catch, and counts clicks per ad and minute in event time.
- The streaming job writes (ad, minute) rows to a columnar store, and updates them when late clicks arrive. The store rolls minutes up into hours and days.
- The query API reads ranges and rollups from the columnar store, filters them by country and device, and serves the advertiser dashboard.
- The click log is also copied hourly to a raw archive in object storage, kept for 30 days.
- A daily recount reads the raw archive, counts each click id once and applies rules that need a whole day of clicks.
- The recount sends billed clicks to billing, which invoices from the recount and explains every difference from the streaming totals. A dashed arrow from the recount back to the columnar store carries corrections.
Step through one made-up click, k42 on ad-7, along the four paths:
The drawing shows the whole scene; its 12 steps are listed below.
Every step as text
- The click. A person clicks an ad for ad-7 at 10:00:05. The link behind the ad is the click service with a signed token, so the browser sends GET /c/{token}.
- Checked, then logged. The click service checks the token's signature and takes the click id, k42, from it. It appends the raw click to the click log and waits until the log has it.
- Then redirected. Only now does the service answer 302 Found, with the advertiser's address in the Location header. Nothing on this path counts anything, so counting can never slow a redirect.
- The person moves on. The browser follows the redirect to the advertiser's page. Everything after this reads the click log, later.
- The stream reads the click. The streaming job reads k42 in log order. It has not seen that id before and no bot list names the source, so it adds one click to ad-7 for 10:00, the minute in which the click happened.
- The minute is published. Once the watermark passes 10:01, the job writes the row for ad-7 and 10:00 to the columnar store. A late click for that minute updates the row later.
- A dashboard reads rows. The advertiser's dashboard asks the query API for the last hour. The API reads minute rows, never raw clicks, and hour or day rows for longer ranges.
- Copied to the archive. Every hour the click log is copied to object storage, where raw clicks stay for 30 days, long enough to recount any day an advertiser disputes.
- The day is recounted. Once the day is closed, the recount reads the archive: k42 is counted once however many times it was delivered, with the same bot lists and rules that need a whole day of clicks.
- Billed from the recount. The recount's totals go to billing; they are the numbers on the invoice.
- Differences flow back. Where the recount differs from the stream's minute rows, it writes corrections to the columnar store, so dashboards end the day on the billed numbers.
- One log, read twice. Only the click service and the click log were on the person's path. The stream and the recount both read the same raw clicks, one for fresh counts and one for exact ones.
The write path. The click service checks the token, appends the raw click to the click log and only then redirects, so an acknowledged click is never lost. The log is partitioned by ad and replicated. Nothing on this path counts anything, so a slow aggregation never slows a redirect.
The streaming path. A streaming job reads the log, removes repeated deliveries by click id and drops clicks from known bots and data-centre addresses. It adds each remaining click to its ad’s count for the minute it happened. When its watermark (stream processing): A stream job's running estimate of how far event time has progressed. When it passes the end of a window, the window's result is emitted, and an event for that window that comes afterwards is late. More in “Stream processing: windows, event time, watermarks”, its estimate of how far event time has progressed, passes the end of a minute, it writes that minute’s rows to a columnar store, and it updates them when late clicks arrive.
The read path. Dashboards ask the query API, which reads minute rows for recent ranges and hour or day rows for long ones, never raw clicks.
The billing path. The log is copied to object storage every hour. Once a day is closed, a batch job recounts it: each click id once, the same lists, and rules that need a whole day, such as a device that clicked one advertiser’s ads a hundred times. Its totals are billed, and every difference from the streaming totals is explained.
Checkpoint
Check: The four paths of one click
Practise: the first design, one question at a time
Practise the four ideas above, the weakest first:
Practice that adapts to you
Practise this lesson
13 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The deep dives this design must answer
The choices left open at each step:
- Removing repeats: an exact store keeps recent click ids and decides every delivery. A Bloom filter answers “maybe seen” from a few bits per id, and is sometimes wrong.
- Late clicks: the job can drop a late click, update a published minute within an allowed lateness, or send the click to a correction path.
- Exact totals: a recount reads raw clicks again. A transactional sink writes results with their log positions. An idempotent write ignores what it already has.
- Serving: a columnar store with rollup tables answers range queries. Counters in a key-value store answer one key fast but scan ranges slowly.
The deep dives answer six questions: removing repeats without dropping valid clicks, invalid clicks, how wrong a first count is, one popular ad, closing a day, and ten and a hundred times the load.
Repeated deliveries and invalid clicks, removed and measured Pro: opens with a Pro, Premium or Ultimate pass
A click can reach the log more than once, and it can come from something that is not a person; both inflate a bill. This part measures what an exact store of click ids and a Bloom filter each cost and miss, and sorts invalid traffic into what the stream can catch and what only a recount can.
In this part:
- Where repeats come from, and what removing them costs, with numbers
- Invalid traffic: lists in the stream, day-long rules in the recount
1 runnable example (Python) · about 10 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Design a real-time leaderboard
Counting minutes in event time: watermarks, late clicks and a hot ad Pro: opens with a Pro, Premium or Ultimate pass
A minute’s count can be published quickly or completely, never both, so the design publishes early and corrects later. This part measures how wrong first results are for four watermark bounds, what allowed lateness costs in state, and how to split an ad that suddenly takes 40 % of all clicks.
In this part:
- How wrong a minute’s first count is, by watermark bound, measured
- Allowed lateness and the correction path, with the state each costs
- A hot ad split over eight sub-keys, worked through
1 diagram · 2 runnable examples (Python) · about 11 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Closing the day for billing: reconciliation, failure drills and levels Pro: opens with a Pro, Premium or Ultimate pass
An invoice must be explained click by click, which a streaming total cannot do on its own. This part reconciles a day of streaming totals with a recount, drills the failures behind each kind of difference and sets out what each level of candidate is expected to cover.
In this part:
- Streaming totals against a recount, every difference explained
- Which guarantee each stage gives, end to end
- Failure drills
- What breaks at ten and a hundred times the load
- Options considered and rejected
- What each level is expected to cover
1 diagram · 1 runnable example (Python) · about 10 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Why does every ad click need its own id, and where in the pipeline are repeated deliveries removed?
Logs and queues deliver at least once: a browser resends a click whose answer it never saw, and a consumer that restarts reads everything after its last committed position again. Without an id, a repeated delivery looks exactly like a second click and is billed twice. Put the id in the signed token of the ad's link, minted for each impression, so every delivery of one click carries the same id and nobody can invent new ones; two clicks on one impression then count once. Remove repeats twice: the streaming job skips ids it has seen recently, which keeps dashboards close to right, and the daily recount counts each id once over the whole day, which makes the bill exact.
-
Mid to senior
App clicks can arrive hours late, dashboards must show each minute within a minute, and invoices must be exact. How do you count clicks per ad and minute, and what happens to a click that arrives after its minute was published?
-
Senior to staff
When a day closes, the streaming total for one advertiser is 10 % higher than the recount from raw clicks, and two others are slightly lower. How do you work out why, which number do you bill, and what do you change?
-
Mid to senior
A single ad goes viral and takes 40 % of 30,000 clicks a second. Your log and streaming job are partitioned by ad id, and one partition processes 5,000 clicks a second. What breaks, and how do you fix it without wrong counts?
Key takeaways
- Ask what each number is used for: dashboards need fresh counts, bills exact ones, kept recountable for 14 days.
- At 864 million clicks a day the peak is 30,000 a second, and raw clicks take 15 times the bytes of minute rows.
- A click id in the signed token of every ad link makes a repeated delivery recognisable.
- Keep every raw click in a log and read it twice: a stream counts minutes in event time, a daily recount bills.
References
- Photon: fault-tolerant and scalable joining of continuous data streams (ACM SIGMOD (Google Research))
- The Dataflow Model: a practical approach to balancing correctness, latency, and cost (Proceedings of the VLDB Endowment (Google Research))
- Windows (Apache Flink documentation) (The Apache Software Foundation)
- Apache Kafka design: message delivery semantics and transactions (The Apache Software Foundation)
- SummingMergeTree table engine (ClickHouse documentation) (ClickHouse)
- Invalid Traffic Detection and Filtration Standards Addendum (Media Rating Council)
- Space/time trade-offs in hash coding with allowable errors (Burton H. Bloom) (Communications of the ACM)
- RFC 9110: HTTP Semantics (302 Found) (IETF)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress