DEV Community

Cover image for Design an ad click aggregator: the system design interview walkthrough
Gowtham Potureddi
Gowtham Potureddi

Posted on

Design an ad click aggregator: the system design interview walkthrough

Design an ad click aggregator: the system design interview walkthrough

The request to design an ad click aggregator sounds like one line — count the clicks on each ad — and that one line hides the whole interview. Picture it for one advertiser and a COUNT(*) settles it; picture it at ad-network scale, where tens of thousands of clicks land every second and an advertiser wants clicks-per-minute for their campaign updated almost live, and you quickly see you can neither scan a billion raw rows per query nor keep a perfect running total in one place. This is one of the most revealing real-time analytics prompts — the Google-Ads-style question — because it is the textbook scaling-writes problem: the firehose of writes dwarfs the trickle of reads, so the entire design bends around ingesting a torrent without losing a click and turning it into numbers you can actually query.

The round turns on one structural insight. "Count the clicks" is not a counter — it is a streaming aggregation pipeline. You cannot afford to store every raw click and re-scan it per query, so you buffer the firehose in a log, pre-aggregate it in a stream processor over time windows, and serve cheap reads from pre-computed rollups (clicks per ad per minute) in an analytics store. And there is a second twist that separates a good answer from a great one: those counts are billing numbers, so "fast but roughly right" is not enough — you add a slower, authoritative path that re-computes the counts from the raw events and reconciles them, which is exactly the lambda architecture (a fast speed layer plus a correct batch layer). Get that reframe — ingest to a log, aggregate the stream, serve rollups, reconcile for accuracy — and the rest of the system design interview is deciding how not to lose a click, how to bucket clicks by the minute they actually happened, and how to keep the totals exact enough to charge money against. This walkthrough follows the path a strong candidate draws on the whiteboard — requirements, estimate, API and data model, high-level design, then the deep dives — naming the follow-up the interviewer uses at each step to tell a memorised answer from real understanding.

Title card for a system design interview walkthrough on designing an ad click aggregator, showing a firehose of click events flowing into a log, a stream processor aggregating them into per-minute rollups for advertiser dashboards, and a parallel batch layer reconciling the counts for billing, on a dark citrus-and-lavender background.

How the ad click aggregator round is actually run

An ad click aggregator round runs about 45 minutes on a shared canvas — you draw the boxes and arrows while you talk, and the interviewer interrupts. It is a favourite because the one-line problem opens onto deep water fast: the moment you draw "count clicks," the question becomes what happens to a single click at scale and how is a per-minute number ready to query, and every answer after that compounds from those two. The clock is a touch tighter than the hour-long rounds, so you have to spend your minutes on ingestion, aggregation and accuracy, not on the ad-serving side.

The interviewer rarely lets "count the clicks" stand. "You're taking in tens of thousands of clicks a second, and an advertiser wants clicks-per-minute for their ad on a dashboard — you're obviously not going to run COUNT(*) over a billion rows per query, so what actually happens to a single click from the moment it lands?" That question — the life of a click in a high-write pipeline — is the one a copied answer can never rehearse, and the one this round is really testing.

The rounds here are modelled on how the loop runs, not scraped from any candidate's recall. What matters for prep is the shape: a blank canvas, a tighter clock, and a follow-up after every decision you make.

The five-step framework for any HLD round

Every high-level design answer fits the same spine. Say it out loud at the start so the interviewer knows you have a plan, then walk it:

  1. Clarify requirements — functional and non-functional, and what you are explicitly not building.
  2. Estimate — clicks per second versus advertiser query rate, to justify every later choice.
  3. High-level design — the boxes and arrows: ingest to a log, aggregate in a stream processor, serve rollups, and a batch path to reconcile.
  4. Deep dive — the two or three decisions that actually decide this system.
  5. Follow-ups — bottlenecks, failure modes, and what you would change at ten times the scale.

What the debrief measures

The score is not "right or wrong design." A good HLD debrief reads on four dimensions, and every move below maps to one: problem solving (did you reframe it as scaling writes — buffer, pre-aggregate, serve rollups — and add a batch layer for accuracy), communication (did you say why a log buffers the firehose and why you bucket by event time, in a single sentence), technical execution (a partitioned, retained log; a stream processor doing event-time windowed aggregation with watermarks and exactly-once; idempotent writes and a reconciling batch job), and time management (did you spend your 45 minutes on ingestion, aggregation and accuracy, not on ad targeting). Reading a walkthrough teaches the design; it does not teach you to deliver it on the clock while someone pushes back.

Step 1 — Requirements: what an ad click aggregator must do

Spend the first few minutes here. Jumping to boxes before you have scoped the problem is the fastest way to design the wrong system.

Type Requirement
Functional Record every ad click; let advertisers query aggregated metrics — clicks per ad over time, e.g. at per-minute granularity; power near-real-time dashboards; the counts are accurate enough to bill on.
Functional (optional) Extra dimensions (geo, device, campaign), click-through-rate, fraud / invalid-click filtering, longer rollup tiers (hourly, daily) — call these out, then scope most of them for later.
Non-functional Very high write throughput with no data loss; low-latency reads over aggregates; accuracy (eventually exact, because it drives billing); horizontally scalable; resilient to spikes and processor failures.

The sentence that earns points: "This is a scaling-writes problem — clicks hugely outnumber queries — so I buffer the firehose in a log, pre-aggregate it in a stream processor into per-minute rollups I can query cheaply, and because these counts are billed, I add a batch layer to re-compute from raw events and reconcile." Say that and you have framed the entire design before drawing a box.

Step 2 — Capacity estimation (back of the envelope)

Estimation here is about proving one thing: writes overwhelmingly dominate reads, so the system is built around ingesting and pre-aggregating, not around querying raw data.

Quantity Assumption Result
Clicks tens of thousands per second at peak, bursty a firehose of writes — the scaling axis; it must be buffered and never dropped
Advertiser queries a comparatively tiny number of dashboard reads reads are trivial if they hit pre-aggregated rollups, not raw events
Raw event volume billions of click rows accumulating too large to scan per query — archive raw to a data lake, serve from rollups
Aggregation granularity per-minute buckets per ad a bounded number of small rollup rows — cheap to store and query

Two conclusions fall out, and you should say both out loud. First, writes are the whole problem — the click rate dwarfs the query rate, so the architecture (a log to absorb the firehose, a stream processor to pre-aggregate, partitioning by ad id) exists to handle write throughput without losing data. Second, reads are easy once you pre-aggregate — an advertiser querying clicks-per-minute reads a handful of small rollup rows, never a billion raw clicks, so the read path is almost an afterthought if the write path did its job. You have now pointed the interview at the real deep dives — ingestion, aggregation, and accuracy — before anyone asked.

Step 3 — API and data model

The API is deliberately small: clicks go in, aggregated metrics come out.

Operation Shape
record_click(ad_id, user_id, ts, metadata) fired when an ad is clicked; appended to the log as a raw event — fast and fire-and-forget.
get_metrics(ad_id, time_range, granularity) returns aggregated clicks over the range (e.g. per minute) from the rollup store.

The data model is thin, and split by purpose: raw events are the durable truth; rollups are the query surface.

Entity Notes
click_event (ad_id, user_id, event_ts, metadata) — the raw click, held in the log (short-term) and archived to a data lake (long-term) for reprocessing.
aggregate_rollup (ad_id, window_start, count) — pre-computed per-ad, per-window counts in an OLAP / time-series store; this is what advertiser queries read.

Step 4 — High-level design: ingest, aggregate, query (and reconcile)

Now the boxes. A click's whole life is a pipeline from a lightweight ingest service, through a log, into a stream processor, out to rollups — with a second, slower path for correctness:

  • Click-processing service: receives record_click and does almost nothing but append the event to a log — stateless and fast, so it can absorb the firehose.
  • Log (Kafka / Kinesis): the backbone. It buffers spikes, decouples ingest from the slower aggregator, is partitioned by ad id, and is replicated and retained (e.g. 7 days) so nothing is lost and a crashed consumer can replay.
  • Stream processor (Flink / Spark Streaming): consumes the log and aggregates clicks per ad per time window, then flushes rollups to the analytics store. This is the speed layer.
  • OLAP / time-series store: holds the small per-ad, per-minute rollups; a query service serves advertiser dashboards cheap reads over them.
  • Data lake + batch job (the batch layer): raw events are also archived to a data lake (S3), where a periodic batch job re-aggregates them from scratch and reconciles the rollups — the authoritative, billing-grade numbers.

Say explicitly that this is a write-side aggregation pipeline with two layers: a fast stream path for low-latency approximate counts, and a slower batch path for exact, billable counts, both feeding the same rollups. Naming that — log, stream processor, rollups, and a reconciling batch job — is a signal the interviewer is listening for.

An ad click aggregator architecture diagram showing a browser click hitting a click-processing service that appends the event to a distributed log partitioned by ad id, a stream processor consuming the log and aggregating clicks per ad per time window before flushing rollups to an OLAP time-series store that a query service serves to advertiser dashboards, and a parallel path where raw events are archived to a data lake and a periodic batch job re-aggregates and reconciles the rollups.

Deep dive: ingesting the click firehose without losing data

This is the first decision that actually decides the round. The follow-up is about the spike: a viral moment triples the click rate and your aggregator falls behind — do you drop clicks?

The answer is no, and the reason you do not have to is the log. The click-processing service does the minimum — append each click to a distributed log like Kafka or Kinesis — and the log does the heavy lifting that makes the firehose survivable:

Log property What it buys
Buffering the log absorbs bursts; if the aggregator falls behind, clicks queue durably instead of being dropped — the processor catches up when the spike passes.
Decoupling ingest and aggregation scale independently; a slow consumer never backs pressure onto the click-processing service.
Partitioning by ad id clicks for an ad land on the same partition, so aggregation parallelises across partitions and ordering per ad is preserved.
Replication + retention the log is replicated across brokers/zones (no loss if a node dies) and retained for days, so a crashed consumer replays from its last offset.

The reason a spike cannot force you to drop clicks is that the log is durable storage, not a transient pipe — it holds the events until the processor has consumed them, and keeps holding them for the retention window even after. If the stream processor crashes, it comes back and resumes from its committed offset, re-reading anything it missed. Saying plainly that the log is the buffer and the safety net — not just a message bus — is the sentence that separates a real answer from a hand-wave.

"A traffic spike triples the click rate and your aggregator falls behind — do you drop clicks?" No — the click-processing service only appends each click to a durable, replicated log like Kafka, which buffers the spike. The aggregator consumes at its own pace and simply catches up once the burst passes; because the log is retained for days, even a crashed processor replays from its last offset and loses nothing. The log is the buffer and the safety net, so backpressure never reaches ingestion.

System Design
HLD · Media & analytics · 45 min
Design an ad click aggregator out loud, with follow-ups

Practise on Zynter →

Deep dive: aggregating the stream over time windows

Ingestion keeps every click; this turns the stream into numbers. The follow-up is the one that trips people: a click that happened at 11:59 doesn't arrive until 12:01 — which minute does it count in, and how do you even know the 11:59 window is finished?

The job is to aggregate clicks per ad, per time window — tumbling one-minute buckets — and the subtlety is which time you bucket by. A stream processor like Flink (or Spark Streaming) consumes the log and does this properly:

  • Event time, not arrival time: bucket each click by when the click actually happened (event_ts), not when the processor received it. A click from 11:59 that arrives at 12:01 still counts in the 11:59 window — otherwise network lag would smear your numbers across the wrong minutes.
  • Watermarks close windows: a watermark tracks how far event time has progressed, so the processor knows when it is safe to say "the 11:59 window is done" and flush it — while still tolerating a bounded amount of late, out-of-order arrival.
  • Exactly-once, fault-tolerant state: the processor keeps running per-window counts as managed state with exactly-once guarantees and recovers that state after a failure — which is why you reach for a real stream processor rather than a hand-rolled consumer keeping a count in memory.
  • Flush rollups to the store: when a window closes, its per-ad counts are written to the OLAP / time-series store as rollups. You can shrink the window or flush more often to trade latency against completeness — tighter windows mean fresher dashboards.
  • Hot ads: a viral ad can hotspot one partition; split the hot key into sub-keys (adA#0 … adA#n) for a first aggregation pass and merge in a second — two-stage aggregation — so no single task is a bottleneck.

The point to state plainly: event time plus watermarks is what makes the counts correct under the messiness of a real network, where clicks arrive late and out of order. A naive per-minute consumer that counts by arrival time is easy to describe and quietly wrong; saying why you bucket by event time is the signal of someone who has thought about streaming, not just drawn it.

"A click from 11:59 arrives at 12:01 — which minute does it count in, and how do you know the 11:59 window is done?" It counts in 11:59, because the processor buckets by the click's event time, not its arrival time. A watermark tracks how far event time has advanced, so once it passes 11:59 (allowing a small grace for late arrivals) the processor knows that window is complete, flushes its per-ad counts to the rollup store, and still slots a slightly-late click into the right bucket. Event time plus watermarks is what keeps the minute-by-minute numbers honest.

A windowed stream aggregation diagram showing click events each tagged with the event time they occurred, sorted into per-minute tumbling window buckets by event time rather than arrival time, a late click from the 11:59 minute arriving at 12:01 still landing in the 11:59 bucket, and a watermark marking how far event time has progressed so the processor knows when a window is complete and can flush its per-ad count to the rollup store.

System Design
HLD · Media & analytics · 45 min
Defend event-time windowed aggregation out loud

Practise on Zynter →

Deep dive: keeping the counts accurate — idempotency and reconciliation

Windowing gives you fast numbers; this makes them numbers you can put on an invoice. The follow-up is the one with money attached: your stream double-counted a window after a crash, and this number is a bill — how do you make it right?

Two different accuracy problems need two different fixes, and naming both is what separates a senior answer:

  • Idempotency / exactly-once writes (stop double-counting): the classic failure is the processor flushing a window's rollup to the store but crashing before committing its stream offset — on restart it reprocesses the same window and over-counts. Fix it by making the flush idempotent: commit the aggregated result and the stream offset atomically (one transaction, or an upsert keyed by (ad_id, window) so re-applying is a no-op). Now a replay after a crash cannot inflate the count.
  • Batch reconciliation (correct the drift): even with idempotent writes, transient processing errors, a bad deploy, or extreme out-of-order events leave the speed layer slightly off. So also archive the raw events to a data lake (S3) and run a periodic batch job (e.g. a daily Spark job) that re-aggregates from the raw events and reconciles — correcting the real-time rollups to the exact counts. The batch result is the billing-grade source of truth.

This pairing — a fast speed layer (the stream) for low-latency dashboards plus a slower batch layer (re-aggregation from raw) for correctness — is the lambda architecture. The dashboard shows near-real-time approximate counts within seconds; the batch job guarantees that what you actually bill is exact. While you are here, name fraud / invalid-click filtering (bots, repeated clicks from one user) as a real concern, even if you scope the detail out — billable clicks have to be valid clicks.

"Your stream double-counted a window after a crash and this number is a bill — how do you make it right?" Two ways working together. First, I make the flush idempotent — the window's count and the stream offset commit atomically, so a replay after a crash can't over-count. Second, I keep the raw events in a data lake and run a periodic batch job that re-aggregates them and reconciles the rollups, so the billed number is recomputed from source and exact. Fast approximate counts from the speed layer, authoritative counts from the batch layer — the lambda architecture.

A Lambda architecture and idempotency diagram showing a fast speed layer where a stream processor writes per-window rollups and commits its stream offset atomically so a crash and replay cannot double-count, alongside a batch layer where raw events archived in a data lake are periodically re-aggregated by a batch job and reconciled against the real-time rollups to produce the billing-grade source of truth.

The follow-ups that decide the ad click aggregator round

Scan the probes from each deep dive and they rhyme. Master these five and you are ready for a version of the question that is not drawn exactly like this one.

  • "A spike triples the click rate — do you drop clicks?" → No: the click-processing service appends to a durable, replicated, retained log that buffers the burst; the aggregator catches up at its own pace and a crashed processor replays from its offset, so nothing is lost. (problem solving)
  • "How do you serve clicks-per-minute without scanning raw events?" → You pre-aggregate on the write side: a stream processor rolls clicks up per ad per minute into an OLAP store, and advertiser queries read small rollup rows, never the billions of raw clicks. (technical execution)
  • "A click arrives two minutes late — which window does it count in?" → The one matching its event time, not arrival time; a watermark tells the processor when a window is safe to close while still admitting bounded late events, so the minute-by-minute counts stay correct. (technical execution)
  • "The stream double-counted after a crash — how do you fix a billed number?" → Make the flush idempotent (commit count and offset atomically so replays don't over-count), and reconcile with a periodic batch re-aggregation from raw events in a data lake — the authoritative total. (communication)
  • "Why keep both a stream and a batch path?" → The lambda architecture: the speed layer gives near-real-time approximate dashboards, the batch layer recomputes exact, billing-grade counts from raw events — fast and correct, instead of choosing one. (problem solving)

Each maps to a debrief dimension. Reading this list teaches you the answers; it does not teach you to deliver them while someone interrupts — that only comes from saying them out loud.

How to prepare: a one-week plan

You do not need fifty systems. You need this one cold and the framework automatic, so the next "design X" is just new nouns in the same five steps.

  • Days 1–2: Learn the five-step framework until it is reflex. Do the ad click aggregator end to end on paper — requirements, estimate, API, data model, and the ingest/aggregate/query/reconcile boxes.
  • Days 3–4: Drill ingestion and aggregation: why a log buffers the firehose and enables replay, and how a stream processor windows clicks by event time with watermarks. Say each tradeoff in one sentence.
  • Days 5–6: Own accuracy — idempotent flushes to stop double-counting, and the batch reconciliation that makes counts billable (the lambda architecture). Answer each of the five follow-ups out loud.
  • Day 7: A full timed 45-minute round, narrating the whole way, with someone (or something) interrupting. This is the rep that counts.

This post is the deep walkthrough behind one row of the broader Top 25 system design interview questions — once the ad click aggregator is automatic, the same spine carries the rest of the list.

Frequently asked questions

How do I design an ad click aggregator step by step?

Follow the five-step HLD framework. Clarify requirements (record every click; let advertisers query aggregated clicks over time, e.g. per minute; near-real-time dashboards; counts accurate enough to bill); estimate to show that writes (tens of thousands of clicks per second) massively dominate reads, so it is a scaling-writes problem; define the small API (record_click appended to a log, get_metrics reading rollups) with a raw-event-vs-rollup data model; draw the design (click-processing service → a partitioned, retained log like Kafka → a stream processor aggregating per ad per window → an OLAP/time-series store of rollups → a query service, plus raw events to a data lake and a periodic batch job to reconcile); then deep-dive ingestion without data loss, windowed aggregation, and accuracy. Finish on the follow-ups about spikes and double-counting.

How do you ingest a high-volume click stream without losing data?

By putting a durable distributed log between ingestion and aggregation. The click-processing service does almost nothing but append each click to a log like Kafka or Kinesis, which buffers spikes, decouples the fast ingest path from the slower aggregator, partitions clicks by ad id for parallelism, and replicates the data across brokers or availability zones so a node failure loses nothing. With a retention window of several days, even a crashed stream processor can replay from its last committed offset and re-read anything it missed. Because the log is durable storage rather than a transient pipe, a traffic spike simply queues up and the aggregator catches up afterward — backpressure never forces you to drop clicks.

How does stream processing aggregate clicks over time windows?

A stream processor such as Flink or Spark Streaming consumes the log and maintains running counts per ad per time window — typically tumbling one-minute buckets — then flushes those rollups to an analytics store. The key subtlety is that it buckets clicks by event time (when the click happened) rather than processing time (when it arrived), so a click delayed by the network still lands in the correct minute. Watermarks track how far event time has advanced so the processor knows when a window is complete and safe to flush while still admitting bounded late events, and its windowed state is kept with exactly-once guarantees and recovered after failures. That is why a purpose-built stream processor is preferred over a hand-rolled consumer counting in memory.

What is the lambda architecture and why reconcile the counts?

The lambda architecture pairs a fast speed layer with a correct batch layer. The speed layer is the stream processor, which produces near-real-time but approximate counts for dashboards. The batch layer archives the raw click events to a data lake and periodically — say daily with a Spark job — re-aggregates them from scratch and reconciles the rollups, producing exact, authoritative numbers. You reconcile because transient processing errors, bad deploys, and extreme out-of-order events can leave the real-time counts slightly off, and in an ad system those counts are billed. Combining both layers lets the system be fast for monitoring and exact for billing, instead of forcing a choice between latency and correctness.

How do you keep ad click counts accurate enough to bill on?

With idempotency on the write side and reconciliation on the batch side. Idempotency prevents double-counting: if the stream processor flushes a window but crashes before committing its offset, a naive restart would reprocess and over-count, so you commit the aggregated result and the stream offset atomically (or upsert keyed by ad and window) to make reprocessing a no-op. Reconciliation corrects residual drift: a periodic batch job re-aggregates from the raw events stored in a data lake and overwrites the rollups with exact totals, which become the billing source of truth. You also filter fraudulent and invalid clicks, since only valid clicks should be billed. Together these make the numbers trustworthy enough to charge against.

How do I practise a system design interview out loud?

Reading a walkthrough is not the same as defending a design while someone interrupts. On Zynter, the HLD track runs live voice rounds modelled on how real system design loops run — you talk through a shared design on the canvas, and a voice interviewer pushes with the follow-ups above one at a time, then gives you a debrief on problem solving, communication, technical execution, and time management. The score is a practice signal, not a hiring decision.

Practise it on Zynter

You can read why the log buffers the firehose. The round is won on saying it under follow-ups.

Pick the Ad Click Aggregator round, draw the ingest-aggregate-query design on the canvas, and defend it out loud to a voice interviewer that keeps asking "a spike triples the clicks — do you drop any?" and "this count is a bill — how do you make it exact?" — then read the debrief. Zynter.ai is on-demand mock interviews modelled on real company loops, with 150+ rounds across 20 companies including the HLD system design track (Sep 2026).

Practise the Ad Click Aggregator round →
Browse all interview tracks

Top comments (0)