Ad Click Aggregation System Design Template (Reference Design)
45-minute path, click dedupe, event-time windows, exactly-once counting, exact and Count-Min top-K, batch reconciliation for billing, failure modes, 10x row
How to use: Use it when designing click, impression or usage counting where some numbers feed invoices. Fill the assumptions and the billing source-of-truth decision first, then the exactly-once and reconciliation sections. The main pitfall is billing from streaming counts that late events and replays can still change.
Preview
System Design: [Ad click aggregation system name]
Reference design for an ad click aggregation pipeline that turns a stream of raw click events into per-ad counts by minute and hour, top-K rankings and billing-grade daily totals. It covers click deduplication, event-time windows and late events, exactly-once counting with Kafka transactions and idempotent sinks, exact and approximate top-K (the Count-Min sketch of Cormode and Muthukrishnan, Journal of Algorithms 55(1), 2005, and the Space-Saving algorithm), and the batch reconciliation that makes streaming numbers safe to bill. It follows the structure of the System Design Document template, adds a 45-minute interview path, access-pattern and failure-mode tables and a cited "How real systems do it" box, and is pre-filled with a worked design to adapt. Replace every
[bracketed]value and every row marked Example; values marked Example are assumptions, not benchmarks.
Document control
| Field | Value |
|---|---|
| Document ID | [SDD-ADC-001] |
| System / feature | [Ad click aggregation and reporting] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| Difficulty | Advanced |
| Billing impact | [Yes: daily totals feed advertiser invoices] |
| Related | [Stream processing engine design], [Invalid traffic filtering spec], [ADR: billing source of truth] |
Approvals
| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| Data platform / SRE | [@name] | [Decision] | YYYY-MM-DD |
| Finance / billing owner | [@name] | [Decision] | YYYY-MM-DD |
| Security and privacy | [@name] | [Decision] | YYYY-MM-DD |
Revision history
| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | Example: Moved billing to reconciled batch totals after reviewer comments on late events |
| 0.3 | YYYY-MM-DD | [@name] | Example: Added Count-Min sketch top-K for the publisher dimension |
| 1.0 | YYYY-MM-DD | [@name] | Example: Approved |
Contents
1. Overview
1.1 Problem statement
[Example: Clicks are counted today by a nightly batch job over web server logs. Advertisers see yesterday's numbers only, budget pacing reacts a day late, and two incidents last quarter double-counted clicks when a log shipper retried a file. The sales team wants live dashboards and a "top ads right now" view, and finance wants one number per ad per day that it can defend in a billing dispute.]
1.2 Proposed solution
[Example: A click service records every click as an event with a unique, signed click ID, redirects the user to the advertiser, and writes the event to a Kafka topic partitioned by ad ID. A stream processor deduplicates click IDs, filters invalid traffic flagged upstream, counts clicks per ad in one-minute event-time windows and writes the counts with Kafka transactions and idempotent upserts into an analytics store. Top-K rankings are exact for the ad dimension and approximate (Count-Min sketch with a heap) for the high-cardinality publisher dimension. Raw events are also archived to object storage, and an hourly and daily batch job recomputes counts from the archive and reconciles them with the streaming numbers. Invoices use only the reconciled daily totals.]
1.3 Decisions requested from reviewers
| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | Example: Which numbers can be billed? | Only reconciled daily totals from the batch path; streaming numbers are labelled provisional | 7.5 |
| Q-02 | Example: How do we stop duplicates? | Signed click ID, keyed dedupe state for 1 h, transactional sink | 7.2, 7.3 |
| Q-03 | Example: How late may a click arrive? | 10 min allowed lateness in the stream; later clicks are corrected by batch | 7.4 |
| Q-04 | Example: Exact or approximate top-K? | Exact for ads; Count-Min sketch plus heap for ad by publisher by country | 7.6 |
| [Q-nn] | [Question] | [Option] | [Section] |
1.4 45-minute interview path
Use this order when the document serves an interview or a short design review. The written sections below are the long form of each step.
| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Scope: count clicks per ad per minute, top-K, billing totals; below-the-line items (2.3) | Three queries the system must answer |
| 5 to 10 | Estimates: clicks per second, distinct keys per window, dedupe state (section 3) | Four numbers, each with its consequence |
| 10 to 15 | Interface: click redirect, metrics query, top-K query (section 4) | Three calls |
| 15 to 25 | High-level design: click service, log, stream aggregator, OLAP store, archive and batch (section 6) | One diagram with a hot and a cold path |
| 25 to 40 | One deep dive: exactly-once counting (default, 7.2 and 7.3), or late events and windows (7.4), or top-K at scale (7.6) | Options, choice, reason |
| 40 to 45 | Reconciliation, failures (aggregator crash, duplicate replay), the 10x row in section 10 | Two failure rows |
Start the default deep dive with the naive answer, a counter per ad incremented in a database on each click, and show the two ways it fails: the row for a popular ad becomes a write hotspot, and a retried request counts twice. Then move to a partitioned log, keyed state and a transactional sink, and say exactly which step removes which duplicate.
2. Requirements
2.1 Core functional requirements
| ID | Requirement | Priority |
|---|---|---|
| FR-01 | Example: Record each click and redirect the user to the landing page | Must |
| FR-02 | Example: Count clicks per ad per one-minute event-time window, with country and device breakdown | Must |
| FR-03 | Example: Roll minute counts up to hourly and daily counts | Must |
| FR-04 | Example: Return the top 100 ads by clicks for the last 1, 5 and 60 minutes, globally and per country | Must |
| FR-05 | Example: Produce reconciled daily totals per ad for billing | Must |
| FR-06 | Example: Exclude clicks flagged as invalid traffic, and keep them countable for audits | Must |
| FR-07 | Example: Return top publishers per ad (high-cardinality, approximate allowed) | Should |
| FR-08 | Example: Recompute any past day from raw events on request | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |
2.2 Non-functional requirements
| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Freshness | Example: Minute counts queryable within 60 s of window close (p95) | Watermark lag plus sink lag |
| NFR-02 | Correctness (billing) | Example: Reconciled daily total equals the deduplicated raw count; zero tolerance | Batch recount vs. invoice input |
| NFR-03 | Correctness (dashboard) | Example: Streaming minute counts within 0.1% of the reconciled count per ad per hour | Hourly reconciliation report |
| NFR-04 | Click latency | Example: Redirect p99 at or below 50 ms; logging never blocks the redirect beyond 5 ms | Click service timing |
| NFR-05 | Durability | Example: No accepted click lost; raw events retained 400 days | Ingest vs. archive counts |
| NFR-06 | Query latency | Example: Dashboard query for one ad, 7 days by hour, p95 under 500 ms | OLAP query logs |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |
2.3 Below the line (out of scope)
Name these explicitly so reviewers and interviewers do not pull the design toward them.
- [Example: invalid traffic and click-fraud scoring models (a separate service sets a flag on the event; see the fraud detection design)]
- [Example: impression counting and click-through rate (same pattern, separate pipeline)]
- [Example: conversion attribution and multi-touch models]
- [Example: the ad auction and budget pacing logic that consume the counts]
- [Item deferred, with the release that will take it]
3. Capacity estimation
3.1 Assumptions
| Input | Symbol | Value | Source |
|---|---|---|---|
| Clicks per day | Example: 1.2 billion | Click logs x growth | |
| Peak to average ratio | Example: 5 | Hourly traffic curve | |
| Bytes per click event (serialised) | Example: 250 B | Event schema in 5.1 | |
| Distinct (ad, country, device) keys per minute | Example: 1 million | Sampled logs | |
| Distinct (ad, country, device) keys per hour | Example: 4 million | Sampled logs | |
| Bytes per aggregate row incl. overhead | Example: 60 B | OLAP store estimate | |
| Dedupe window for click IDs | Example: 1 h | Retry and replay behaviour | |
| Bytes per dedupe entry in keyed state | Example: 60 B | 16-byte ID plus state overhead | |
| Events per second one aggregation task sustains | Example: 10 000 | Benchmark |
3.2 Event rate and bandwidth
At 250 B per event that is 300 GB per day and about 17.4 MB/s at peak. Kafka retention of 7 days with replication factor 3 holds TB. Consequence: bandwidth and log storage are small, so the design is shaped by state and correctness, not by throughput.
3.3 Aggregation tasks and partitions
Run 32 tasks (more than 4x headroom for replays, which must catch up faster than real time) over 128 Kafka partitions. Consequence: create 128 partitions from day one, because changing the partition count remaps ad IDs to partitions and breaks per-key ordering; 128 still covers the 10x case (about 70 tasks) in section 10.
3.4 Dedupe state
That is 250 million click IDs if the whole hour ran at peak, an upper bound. Spread over 32 tasks it is about 0.47 GB per task. Consequence: the dedupe state fits in an embedded on-disk state backend (RocksDB) per task with incremental checkpoints; no external lookup per click is needed.
3.5 Aggregate storage
| Table | Rows per day | Size per day | Retention | Total |
|---|---|---|---|---|
| Minute counts | = 1.44 billion | 86.4 GB | 30 days | 2.59 TB |
| Hourly counts | = 96 million | 5.76 GB | 730 days | 4.2 TB |
| Raw archive (compressed 5x) | 1.2 billion events | 60 GB | 400 days | 24 TB |
Consequence: minute rows are the big table; keep them 30 days in a columnar OLAP store partitioned by day and roll older data up to hours.
3.6 Top-K state
Exact counts for the ad dimension need one counter per key per window: about 1 million keys x 64 B = 64 MB per minute window across the cluster, which is cheap. The publisher dimension (ad x publisher x country) has about 30 million keys per minute, or 1.92 GB per minute window, multiplied by every open window. A Count-Min sketch with and needs columns and rows, 190 281 four-byte counters or about 761 KB per task and window. Consequence: exact top-K for ads, sketch-based top-K for the publisher dimension (7.6).
3.7 Summary
| Quantity | Estimate | Design consequence |
|---|---|---|
| Peak clicks | About 69 000/s | 128 partitions, 32 tasks |
| Raw log | 300 GB/day, 6.3 TB in Kafka | Throughput is not the hard part |
| Dedupe state | Up to 15 GB, about 0.47 GB per task | Local keyed state, no remote lookup |
| Minute rows | 1.44 billion/day | Columnar store, 30-day retention, roll-ups |
| Sketch per task and window | About 761 KB | Approximate top-K for the high-cardinality dimension |
4. API design
4.1 Interfaces
| Interface | Operation | Purpose |
|---|---|---|
| Click endpoint | GET /c/{click_token} | Validate the signed token, log the click, return 302 to the landing page |
| Metrics API | GET /v1/ads/{ad_id}/clicks?from=&to=&granularity=minute|hour|day&group_by=country | Time series for dashboards |
| Top-K API | GET /v1/top-ads?window=1m|5m|60m&k=100&country= | Ranking for the latest closed window |
| Billing export | GET /v1/billing/daily?date=&advertiser_id= | Reconciled daily totals with a reconciliation version |
| Admin | POST /v1/recompute | Recompute a day from the raw archive |
4.2 Click token and event
The click token is minted when the ad is served and signed with a server key (HMAC), so a click ID cannot be forged and the same click always carries the same ID. Redirects are logged before the 302 is returned, but the write is an asynchronous append to a local buffer that is flushed to Kafka; the redirect never waits for a Kafka acknowledgement.
{"click_id": "c_01JD4Q8V6R2M7X9K3T5W0YH1ZB", "ad_id": "ad_48213", "campaign_id": "cmp_902", "advertiser_id": "adv_77", "publisher_id": "pub_5521", "event_time": "2026-10-30T09:14:07.412Z", "country": "DE", "device": "mobile", "ivt_flag": false, "schema_version": 3}
4.3 Top-K response
Every response says which window it describes and whether the numbers are provisional, so a client never mistakes a streaming count for a billable one.
{"window_start": "2026-10-30T09:14:00Z", "window_end": "2026-10-30T09:15:00Z", "k": 3, "status": "provisional", "watermark": "2026-10-30T09:15:31Z", "items": [{"ad_id": "ad_48213", "clicks": 18342}, {"ad_id": "ad_10077", "clicks": 15120}, {"ad_id": "ad_33190", "clicks": 9981}]}
5. Data model
5.1 Entities
Raw click events are immutable; every count is derived from them and can be rebuilt from the archive.
5.2 Storage choices
| Data | Store | Key | Main queries | Why |
|---|---|---|---|---|
| Raw clicks (hot) | Example: Kafka topic, 128 partitions, 7 days | Partition by ad_id | Stream consumption, replay | Ordered per ad, replayable |
| Raw clicks (cold) | Object storage, columnar files by hour | dt, hour | Batch recount, audits | Cheap, immutable, 400 days |
| Dedupe and window state | Embedded state backend per task, checkpointed to object storage | click_id, (ad_id, window) | Point lookups per event | Local, no network hop |
| Minute and hourly counts | Columnar OLAP store | (ad_id, window_start), partitioned by day | Time series per ad, group by country | Fast scans and roll-ups |
| Billing totals | Relational database | (ad_id, day, recon_version) | Invoice generation | Transactions, audit trail |
| Top-K snapshots | Key-value cache | (window_id, scope) | Latest ranking | One read per dashboard refresh |
5.3 Access patterns
| Access pattern | Frequency | Key or index that serves it | Partition key rationale |
|---|---|---|---|
| Deduplicate one click | Every click (about 69 000/s at peak) | Keyed state on click_id in the task that owns the ad | The click ID is checked in the task that owns the ad, so the same click always lands in the same task |
| Add a click to its window | Every click | Keyed state on (ad_id, window_start, country, device) | Partitioning by ad_id keeps all clicks of one ad in one task, so counts need no cross-task merge |
| Dashboard time series for one ad | About 2 000 queries/s | OLAP primary key (ad_id, window_start) | Sort by ad then time makes a 7-day query one contiguous range |
| Latest top-K | Every dashboard refresh | Cache key (window_id, scope) | Precomputed per window, no aggregation at read time |
| Daily billing totals | Once per day per advertiser | Relational index on (advertiser_id, day) | Small table, needs transactions and history |
| Recount one hour from raw | Hourly batch, ad hoc | Object storage path dt=/hour= | Hour partitions let the batch read only what it reconciles |
6. High-level design
There are two paths: a hot streaming path for fresh, provisional numbers and a cold batch path that produces the reconciled, billable numbers.
| Component | Responsibility | Scales by |
|---|---|---|
| Click service | Validate token, redirect, append event to local buffer | Stateless instances behind the load balancer |
| Local event buffer | Batch events and produce to Kafka with idempotence on | With click service |
| Kafka clicks topic | Ordered, replayable log partitioned by ad | Partitions and brokers |
| Stream aggregator | Dedupe, filter invalid traffic, window and count, write transactionally | Tasks up to the partition count |
| Top-K merger | Merge per-task top-K lists and sketches per window | One small job per window size |
| Archiver | Write hourly columnar files to object storage | Consumer group |
| Batch recount and reconciler | Recount from raw, compare, publish billing totals and corrections | Batch cluster size |
| Query API | Serve dashboards, top-K and billing exports | Stateless instances |
7. Detailed design
The deep dives in this section, each with options, choice and reason:
| Deep dive | Options considered | Choice | Reason | Section |
|---|---|---|---|---|
| Duplicate removal | Trust the source; database unique key; keyed dedupe state; Bloom filter | Signed click ID plus keyed dedupe state for 1 h | Exact, local, and a Bloom false positive would drop a real, billable click | 7.2 |
| Exactly-once counting | At-least-once plus idempotent sink; Kafka transactions; two-phase commit to the store | Checkpointed state plus transactional or idempotent upsert sink | Replays after a crash do not change any count | 7.3 |
| Late events | Processing-time windows; event-time windows with watermark; batch only | Event-time windows, 10 min allowed lateness, batch corrects the rest | Counts belong to the minute the click happened | 7.4 |
| Billing source of truth | Streaming counts; batch counts; both reconciled | Reconciled batch totals | One defensible number per ad per day | 7.5 |
| Top-K | Exact counts everywhere; Count-Min sketch plus heap; Space-Saving | Exact for ads, sketch plus heap for publishers | State bounded where cardinality is high | 7.6 |
7.1 Click flow
If the token signature fails, the click service still redirects (users must
never see an error for an ad click) but writes the event with an
invalid_token reason to a separate topic for the fraud team.
7.2 Duplicate removal
Duplicates enter at three places, and each has its own control:
| Source of duplicate | Example | Control |
|---|---|---|
| User or browser | Double click, page reload, prefetch | Same signed click_id; dedupe state drops the second copy |
| Producer retry | Buffer resends a batch after a timeout | Kafka idempotent producer drops the resent records within a partition |
| Consumer replay | Aggregator restarts from the last checkpoint | State and offsets restored together (7.3); output written transactionally |
Dedupe state keeps each click ID for one hour after its event time, with a TTL so state stays near the 15 GB bound in 3.4. A Bloom filter would use less memory, but a false positive drops a genuine click without trace, which is a billing error; exact keyed state is affordable at this scale, so it wins.
7.3 Exactly-once counting
"Exactly once" here means each accepted click changes the published counts exactly once, even when tasks crash and replay. Three mechanisms combine:
- Source: the aggregator reads Kafka with
isolation.level=read_committedand keeps its offsets in its own checkpoints, not in auto-commit. - State: periodic checkpoints (every 30 s) capture offsets, dedupe state and open windows together. After a crash the task restores the last checkpoint and re-reads from the saved offsets, so state and input agree.
- Sink: window results are written either as a Kafka transaction that
commits with the checkpoint (when the next stage is another topic), or as
an idempotent upsert keyed by (
ad_id,window_start,country,device) that overwrites rather than increments (when the sink is the OLAP store). A replayed window writes the same row with the same value.
The rule to review: never write increments (clicks = clicks + n) to an
external store from a stream job; write absolute window values that a replay
can repeat safely.
7.4 Windows, watermarks and late events
Clicks are counted by event_time, not arrival time, in tumbling one-minute
windows. The watermark trails the largest event time seen by 30 s, so a
window emits a provisional result about 30 s after it ends. Windows stay
open for 10 minutes of allowed lateness; a late click updates the window and
the sink overwrites the row with the new absolute value. A click later than
10 minutes goes to a side output that is counted only by the batch path
(7.5). Report the late share as a metric; if it rises, mobile clients are
probably buffering offline.
7.5 Reconciliation and the billing source of truth
The batch job runs every hour for the previous hour and once a day for the
whole day, after a 2-hour delay so stragglers land in the archive. It
deduplicates raw events by click_id, drops invalid traffic and counts per
ad. The reconciler then compares batch and streaming counts per ad and hour:
| Outcome | Rule | Action |
|---|---|---|
| Match | Difference at or below 0.1% (NFR-03) | Mark the hour reconciled |
| Drift | Difference above 0.1% for any ad | Overwrite streaming rows with batch values, open an incident |
| Missing data | Batch count far below stream count | Archiver gap: block billing for the day, page on-call |
Daily billing totals always come from the batch count, stored with a reconciliation version. A correction after invoicing creates a new version and an adjustment line, never an edit in place.
7.6 Top-K
Because Kafka is partitioned by ad_id, every ad's full count lives in one
task. Each task keeps exact counts per window and emits its local top 100;
the merger takes the top 100 of the union, which is exact because no ad is
split across tasks. Five-minute and 60-minute rankings are computed from the
stored minute counts with sliding sums.
The publisher dimension (ad x publisher x country) is too wide for exact counts per window (3.6). Each task keeps a Count-Min sketch of rows and columns plus a min-heap of the current heaviest keys. For a key the estimate is the minimum over rows:
where is the total count in the window. With and a peak minute of million clicks, the overestimate is at most about 417 clicks with probability 0.999. Sketches with the same dimensions and hash functions merge by adding cells, so the merger sums the per-task sketches. The Space-Saving algorithm (Metwally and colleagues, ICDT 2005) is the alternative when only the heavy hitters matter: it keeps a fixed number of counters and gives a guaranteed error bound per counter. Label these rankings approximate in the API.
7.7 How real systems do it
How real systems do it (paraphrased, not copied). Google's SIGMOD 2013 paper "Photon: Fault-tolerant and Scalable Joining of Continuous Data Streams" (Ananthanarayanan and colleagues) describes a system that joins ad click events with the search query events that led to them, across several datacenters at once. The authors state three guarantees: the joined output never contains a duplicate, most joinable events appear in real time, and every event is eventually joined exactly once. They report that it ran through degraded infrastructure and whole datacenter outages without manual steps, handled millions of events per minute at peak with average end-to-end latency under 10 seconds, and fed advertiser billing. The idea reused here is that a shared record of already-processed event IDs, kept consistent across workers, is what prevents double counting when processing is retried or runs in more than one place. The Apache Kafka design documentation explains the transactional pattern in 7.3: offsets are committed in the same transaction as the output, consumers read only committed records, and for external stores the offset is kept together with the output instead of using two-phase commit. Read the originals in 12.3.
8. Scalability and performance
| Concern | Approach | Limit before redesign |
|---|---|---|
| Click rate | Stateless click service, async buffer | Load balancer and Kafka brokers, far above 69 000/s |
| Hot ad (one viral ad) | One partition per ad caps one ad at one task's rate; pre-aggregate per click-service instance for 1 s before producing if needed | Example: about 10 000 clicks/s for a single ad |
| Dedupe state | Local keyed state with TTL | Example: about 150 GB at 10x, still local |
| Replay speed | 4x task headroom | Must catch up a 1-hour backlog in under 15 min |
| OLAP query load | Pre-aggregated rows, sort key by ad and time | Example: 2 000 queries/s per replica set |
Freshness budget for a minute window (target NFR-01, 60 s after window close):
| Step | Budget |
|---|---|
| Click buffer flush to Kafka | 1 s |
| Watermark delay | 30 s |
| Window emit and sink write | 5 s |
| OLAP visibility | 10 s |
| Headroom | 14 s |
9. Reliability, security and operations
9.1 Failure modes
| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Aggregation task | Crash or node loss | Consumer lag per partition | Ads on that task's partitions, counts delayed | Restore from checkpoint, replay from offsets; no double count (7.3) | Example: 2 min |
| Kafka broker | Broker down | Under-replicated partitions | None with replication factor 3 and min.insync.replicas=2 | Leader election | Example: 30 s |
| Click buffer | Click service instance dies with unflushed events | Ingest count vs. redirect count | Up to 1 s of clicks on one instance | Small flush interval; buffer on local disk for durability | Example: 1 s of data at risk |
| OLAP store | Region unavailable | Query error rate | Dashboards only, billing unaffected | Replica in second zone; stream sink retries idempotently | Example: 15 min |
| Archiver | Falls behind or drops files | Hourly raw count vs. Kafka offsets | Batch recount and billing for that hour | Block billing until counts match; replay from Kafka within 7 days | Example: 1 h |
| Bad deploy of aggregator | Wrong counts | Reconciliation drift above 0.1% | Every ad, provisional numbers | Roll back, replay from last good savepoint, batch overwrites | Example: 1 h |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [Time] |
9.2 Security, privacy and abuse
| Threat | Control |
|---|---|
| Forged or replayed click tokens | HMAC-signed tokens with expiry; key rotation; replays share a click ID and are deduplicated |
| Click fraud and bots | Invalid traffic flag from a separate service; flagged clicks counted separately, never billed |
| Personal data in click events | Keep only coarse country and device class; no raw IP in the aggregation path; IP kept in a restricted fraud store with short retention |
| Advertisers reading other advertisers' data | Query API scopes every request by advertiser_id from the caller's token |
| Tampering with billing totals | Billing DB writes only from the reconciler; versioned, append-only adjustments, audited |
| [Threat] | [Control] |
Data protection law applies to click data that can identify a person. For example, the EU GDPR storage-limitation principle (Article 5(1)(e)) and the UK GDPR equivalent require that identifiable data is not kept longer than needed. This is a design note, not legal advice; check the current official text and a qualified professional for your jurisdictions.
9.3 Observability
| Signal | Metric or log | Alert threshold |
|---|---|---|
| Consumer lag | Seconds behind per partition | Above 120 s for 5 min |
| Watermark lag | Now minus watermark | Above 90 s |
| Late events | Share of clicks into side output | Above 0.5% of an hour |
| Duplicate rate | Share of clicks dropped by dedupe | Doubling vs. 7-day baseline |
| Reconciliation drift | Max per-ad difference per hour | Above 0.1% |
| Checkpoint duration | p99 per job | Above 50% of the checkpoint interval |
9.4 Cost estimate
| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Kafka brokers | [$/broker-month] | Example: 6 | [$] |
| Stream processing workers | [$/vCPU-month] | Example: 32 tasks on 8 workers | [$] |
| OLAP store | [$/node-month] | Example: 6 nodes, 7 TB | [$] |
| Object storage for raw archive | [$/TB-month] | 24 TB | [$] |
| Batch recount | [$/vCPU-hour] | [hours/month] | [$] |
| Total | [$] |
10. Trade-offs and alternatives
| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Architecture | Batch only; stream only (kappa); stream plus batch reconciliation | Stream plus batch reconciliation | Fresh dashboards and defensible billing | Two code paths to keep in agreement |
| Counting store | Increment counters in a database; aggregate in stream and upsert | Aggregate in stream, upsert absolute values | No hot rows, replay-safe | Results appear per window, not per click |
| Dedupe | Bloom filter; keyed exact state; database unique constraint | Keyed exact state | No false drops, no remote call | About 15 GB of state at peak |
| Top-K | Exact everywhere; sketch everywhere | Exact for ads, sketch for publishers | Exactness where cheap, bounded state where wide | Publisher rankings are approximate |
| Late data | Drop; long lateness in stream; batch correction | 10 min in stream, batch for the rest | Bounded state | Stragglers appear only after the batch run |
| What changes at 10x / 0.1x | 10x (about 694 000 clicks/s at peak): about 70 tasks within the 128 partitions, dedupe state about 150 GB, pre-aggregate hot ads in the click service, and consider sketch-based top-K for countries too. 0.1x (about 6 900/s): one Kafka cluster of 3 small brokers and a single stream worker, or even a database with a unique index on click_id and a nightly recount | Review at each doubling of peak clicks | State and replay time grow first, not bandwidth | [Note] |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |
11. Delivery plan and risks
11.1 Milestones
| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Click service, Kafka topic, archiver | Raw counts match the legacy log job for 14 days | YYYY-MM-DD |
| M2 | Stream aggregator and OLAP store in shadow | Hourly drift at or below 0.1% for 14 days | YYYY-MM-DD |
| M3 | Dashboards and top-K live, labelled provisional | Freshness target met for 7 days | YYYY-MM-DD |
| M4 | Billing from reconciled totals | Finance sign-off after one parallel billing month | YYYY-MM-DD |
11.2 Rollout and rollback
[Example: Run the new pipeline beside the legacy batch job for a full billing month and compare invoices line by line. Dashboards switch first; billing switches only after finance approves the parallel run. Rollback for the stream path is a redeploy from the last savepoint; billing can fall back to the legacy job until it is decommissioned.]
11.3 Risks
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | Example: Stream and batch logic diverge (different dedupe or IVT rules) | Medium | High | Shared rule library and the same test fixtures for both paths | [@name] |
| R-02 | Example: A viral ad overloads one partition | Medium | Medium | Click-service pre-aggregation, alert on per-partition lag | [@name] |
| R-03 | Example: Advertisers dispute provisional numbers | High | Medium | Label provisional, show reconciliation status per day | [@name] |
| R-04 | Example: Archive gap discovered after the Kafka retention window | Low | High | Archive-vs-offset check every hour, 7-day retention | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |
12. Appendices
12.1 Open questions
| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | Example: Should invalid-traffic decisions made after invoicing trigger credits automatically? | [@name] | YYYY-MM-DD |
| OQ-02 | Example: Do advertisers need minute data beyond 30 days? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |
12.2 Glossary
| Term | Meaning |
|---|---|
| Click ID | Unique, signed identifier of one click, minted when the ad is served |
| Event time | When the click happened, carried in the event |
| Watermark | The processor's estimate that no older events are still to come |
| Allowed lateness | How long a window accepts late events after the watermark passes |
| Exactly-once (effective) | Each accepted event changes published results once, despite retries and replays |
| Reconciliation | Comparing streaming counts with a batch recount and correcting the difference |
| Count-Min sketch | Fixed-size table of counters that overestimates counts by a bounded amount |
| Heavy hitter | A key whose count is a large share of the total |
| Invalid traffic (IVT) | Clicks judged not to come from genuine user interest |
| [Term] | [Meaning] |
12.3 References
| Reference | Use |
|---|---|
| Cormode G., Muthukrishnan S., "An improved data stream summary: the count-min sketch and its applications", Journal of Algorithms 55(1), pp. 58-75, April 2005, doi:10.1016/j.jalgor.2003.12.001 | Sketch dimensions and error bound in 3.6 and 7.6 |
| Metwally A., Agrawal D., El Abbadi A., "Efficient Computation of Frequent and Top-k Elements in Data Streams", ICDT 2005, LNCS, pp. 398-412, doi:10.1007/978-3-540-30570-5_27 | Space-Saving alternative in 7.6 |
| Ananthanarayanan R. et al., "Photon: Fault-tolerant and Scalable Joining of Continuous Data Streams", SIGMOD 2013, pp. 577-588, doi:10.1145/2463676.2465272 | 7.7 |
| Apache Kafka documentation, Design: message delivery semantics, idempotent producer and transactions since 0.11.0.0 | 7.2, 7.3 |
Stream Processing Engine System Design (system-design/design-stream-processing-engine) | Windows, watermarks, checkpoints |
Message Queue System Design (system-design/design-message-queue) | Partitioning and retention of the clicks topic |
Fraud Detection System Design (system-design/design-fraud-detection) | Invalid traffic flag |
Gaming Leaderboard System Design (system-design/design-gaming-leaderboard) | Exact ranking patterns |
Change Data Capture Pipeline Design (system-design/design-cdc-pipeline) | Exactly-once delivery into downstream stores |
Back-of-Envelope Estimation and Capacity Model (system-design/back-of-envelope-estimation) | Method for section 3 |
Standard and licence note
Based on the published algorithms and documentation cited in 12.3: the Count-Min sketch (Cormode and Muthukrishnan, 2005), the Space-Saving algorithm (Metwally and colleagues, 2005) and the Apache Kafka delivery semantics documentation. The Photon paper is paraphrased in 7.7 only; no text, figures or code were copied, and every number in this template is an illustrative example computed for this design. Check the current Kafka documentation for your version before relying on a configuration name. The note on data protection in 9.2 is not legal advice. Original text written for this collection. Template licence: MIT.
Related templates
Zanzibar-Style Authorization Service System Design Template
Relationship-based authorization service: relation tuples, namespace config, Check/Expand/List/Watch APIs, consistency tokens, caching, sharding, failure modes
System DesignCDC Pipeline System Design Template
CDC pipeline: log-based capture, watermark chunked snapshot, per-key order, schema evolution, outbox, search, cache and warehouse sinks, replay
System DesignChat System Design Template (Reference Design)
Real-time 1:1 and group chat: WebSocket gateways, per-conversation ordering, fan-out, receipts, offline sync, push, E2EE