Design a distributed counter for likes and views
The interviewer asks
“Design the system that counts likes and views for posts and videos on a large social or video platform. Popular items can receive hundreds of thousands of views per second, counts must show quickly on screen, and a user should not be able to like the same post twice.”
What a strong answer looks like
A strong answer separates two very different counters. Likes are a set membership problem (did user U like post P?) with a derived count: store the like relationship durably with a unique key, and update the count asynchronously. Views are a high-volume, approximate counter where some error is acceptable: never do one database write per view; instead aggregate in memory and in a stream processor, then write totals in batches. Handle hot items with sharded counters or local pre-aggregation, serve counts from a cache, and decide which consistency the user really needs (their own like must show immediately; the global total can lag by seconds). Senior candidates add bot and duplicate filtering for views, idempotent consumers so replays do not double count, HyperLogLog for unique viewers, and periodic reconciliation from the source of truth.
Concepts to mention
1 Requirements
Functional
- Like and unlike a post; a user can like a post at most once
- Show like count and whether the current user liked it
- Record a view when content is watched or shown (e.g. video played for 3 s)
- Show view count on posts and videos, rounded for large numbers (1.2M)
- Creators see accurate daily totals in analytics
- Count unique viewers per video per day
Non-functional
- Write scale: 1M view events/s peak, 100k like/unlike events/s peak
- Read scale: 5M count reads/s (every feed item shows counts)
- Count read latency p99 under 10 ms
- Like action: the user sees their own like immediately; global counts may lag up to ~5 s
- View counts may lag up to 1 minute and be within ~1% before reconciliation; creator analytics exact within a day
- Availability 99.95%; a counter outage must never break the feed
Out of scope (say it out loud)
- Feed ranking that uses these counts
- Monetisation payout calculations (they consume reconciled daily totals)
- Comments
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
PUT/v1/posts/{id}/like | Idempotent like. Returns {liked: true, like_count}. Repeating it does nothing. |
DELETE/v1/posts/{id}/like | Idempotent unlike. |
POST/v1/events/views | Batched view events from clients: [{item_id, ts, watch_ms, session_id}], sent every few seconds. |
GET/v1/counts?ids=p1,p2,... | Batch read of like/view counts plus 'liked by me' flags for a feed page. |
GET/v1/creators/{id}/analytics?date= | Exact daily views, likes and unique viewers from the warehouse. |
4 Data model
likesCassandra / ScyllaDBpartition (post_id)clustering (user_id)liked_atSource of truth for who liked what; the primary key makes a double like impossible (an insert is an upsert). A second table partitioned by user_id answers 'did I like these posts' and 'my liked posts'.
countsCassandra (or MySQL sharded) + Redis cacheitem_id (PK)like_countview_countupdated_atWritten in batches by aggregators with absolute values or idempotent increments. Redis holds hot items with a short TTL; reads go Redis → DB.
view_eventsKafka (topic partitioned by item_id)event_iditem_iduser_or_device_idtswatch_msclient infoDurable buffer; 3-day retention for replay. Partitioning by item_id lets one aggregator own each item's count.
unique_viewersRedis HyperLogLog / Druid sketcheskey = uv:{item_id}:{date}~12 KB HLLEstimates unique viewers with ~0.81% standard error; merges across days and shards.
daily_rollupsData warehouse (BigQuery/ClickHouse) + S3item_iddateviewslikesunique_viewersExact batch recompute from raw events each night; used for creator analytics and to correct drift in the online counters.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
User likes a post
- 1Client calls PUT /v1/posts/p1/like and immediately shows the filled heart and count + 1 (optimistic UI).
- 2Like service writes (post_id=p1, user_id=u7) to the likes tables. If the row already existed, it returns success without emitting anything.
- 3To know if the row is new, it uses a lightweight transaction / conditional insert (IF NOT EXISTS) or reads first; only a real state change emits an event.
- 4It publishes {event_id, post_id, delta: +1} to Kafka keyed by post_id.
- 5Flink sums deltas per post over 2-5 s windows and applies one increment per post to the counts table, recording the last processed offset with the update for idempotency.
- 6Redis count for p1 is updated; other users see the new count within a few seconds.
Views of a viral video
- 1Clients send view events in batches every 10 s (fewer requests on slow 3G/4G networks in smaller cities).
- 2Ingest service validates, drops obvious bots (rate per device, known bad IPs), adds event_id and writes to Kafka keyed by item_id.
- 3A hot item is detected; the ingest layer spreads its events over 16 sub-keys (item#0..#15) so no single partition is overloaded.
- 4Flink keeps counts in state per key, de-duplicates by event_id within the window, and every 10 s emits a total per item.
- 5It writes the item's new total to the counts table (one write per item per window) and to Redis, and updates the HyperLogLog for unique viewers.
- 6Nightly, the batch job recomputes exact views from raw events in S3 and corrects any drift in the counts table.
7 Deep dives
Why not UPDATE count = count + 1
The naive design runs UPDATE posts SET views = views + 1 WHERE id = ? on every view. On a viral video at 200k views/s, every update needs the same row lock, so they run one after another; a single row in MySQL or Postgres manages maybe a few thousand such updates per second before lock waits and replication lag explode. Redis INCR is much faster (100k+/s per key) but one key still lives on one shard and one CPU core, and all the network traffic for that item hits one node. The general fix is to aggregate before writing. Option one: sharded counters, where the count for item X is stored as N sub-counters (X#0..X#N-1); each increment picks a random shard and reads sum all N. Google's App Engine documentation popularised this pattern. Option two: pre-aggregate in the write path: app servers or a stream processor sum increments in memory for a few seconds and write one delta per item per interval. At 1M events/s across 50k active items, this cuts writes to ~10k/s. The cost is lag of a few seconds and possible loss of an in-memory window on crash unless the source is a replayable log like Kafka.
Likes: correctness of the set, approximation of the count
The thing that must be exactly right for likes is membership: a user has either liked a post or not, and must never be counted twice. Store this as a row keyed by (post_id, user_id). Because the key is unique, a retried request or a double tap is naturally idempotent. The count is derived data. Emit a delta event only when the state actually changes (like on a not-liked post, or unlike on a liked one), otherwise retries would inflate counts. Applying deltas to the count must also be idempotent: Kafka consumers can reprocess messages after a crash. Flink with checkpoints gives exactly-once state inside the job; to make the sink effectively-once, either write absolute totals (set count = 1,234,567, which is safe to repeat) or store the last applied offset per partition in the same transaction as the counts. Small drift can still creep in (bugs, lost events), so a periodic job recounts likes from the source table for active posts and fixes the counter. The UI uses the user's own like state from the likes table plus an optimistic +1, so the user sees a consistent picture even while the global count lags.
What counts as a view, and unique viewers
A view is a product definition, not a technical one: YouTube-style systems count a view only after a minimum watch time, and filter repeated views from the same viewer and bots. This matters because views often drive creator payouts and ad billing. So the pipeline has two tiers: a fast, approximate counter that shows on screen within seconds, and an exact, audited daily count computed in batch from raw events after fraud filtering. Unique viewers are a cardinality problem: storing every viewer ID per video is too much memory for billions of items. HyperLogLog estimates distinct counts using ~12 KB per counter in Redis (PFADD/PFCOUNT) with about 0.81% standard error, and two HLLs can be merged, so you can compute weekly uniques from daily ones. For exact uniques in creator analytics, run COUNT(DISTINCT) in the warehouse overnight. Explain both and when each is used; interviewers like candidates who treat 'view count' as a product and integrity question, not only a scaling one.
8 Trade-offs
Freshness vs write cost
Flushing aggregates every 1 s gives near-real-time counts but 10× the DB writes of a 10 s window. For views, 10-30 s is fine. For likes, 2-5 s keeps the experience 'live' while cutting writes. Users mostly care about their own action, which the UI shows immediately.
Sharded counters vs stream aggregation
Sharded counters are simple and synchronous but make reads N times more expensive and do not de-duplicate. Stream aggregation (Kafka + Flink) handles huge volume, dedupe and windows, but adds infrastructure and seconds of lag. Large platforms use stream aggregation for views and sometimes sharded counters for moderate-volume likes.
Exact vs approximate
Showing '1.2M views' does not need exact numbers, so approximate online counters are fine. Money-related numbers (payouts, ad billing) must come from the reconciled batch pipeline. Making the online path exact would cost far more for no user benefit.
9 Bottlenecks
Hot partition in Kafka
Keying by item_id sends a viral item's events to one partition and one Flink task. Detect hot keys and salt them (item#0..#15), aggregate partial sums in parallel, then combine with a second, small aggregation step.
Count reads for feeds
5M reads/s for counts on every feed card. Batch reads (one MGET per feed page), cache hot items in Redis and in the feed service's local cache for a few seconds, and accept slightly stale numbers.
10 Failure handling
Flink job crashes
It restarts from the last checkpoint (e.g. every 30 s) and reprocesses Kafka from the checkpointed offsets. Because the sink writes absolute totals or tracks applied offsets, reprocessing does not double count. Counts on screen freeze for a minute, then catch up.
Redis cache lost
Count reads fall back to the counts table; the read service limits concurrency and serves 'no count' for non-critical surfaces if the DB is overloaded. The feed never fails because of counts.
Like service writes the row but fails to publish the event
Use a transactional outbox or CDC from the likes table (e.g. Debezium on the DB change log) so the event is derived from the committed write; otherwise the count drifts. The nightly recount corrects any remaining drift.
11 What impresses
- Treats likes and views as different problems with different correctness needs
- Quantifies why one row UPDATE per view fails
- Only emits like events on real state change, so retries do not inflate counts
- Makes the sink idempotent (absolute totals or stored offsets) and explains why
- Mentions HyperLogLog with its memory and error numbers
- Two-tier counts: fast approximate on screen, exact reconciled for payouts
12 Mistakes
- Incrementing a single DB row per event
- Storing only a counter for likes, so you cannot prevent double likes or unlike
- Assuming Kafka 'exactly-once' makes database counts exact end to end
- Requiring strong consistency for global counts that users cannot even verify
- Ignoring bots and repeated views
13 Follow-ups
Lessons this design uses
Practice questions
- A short-video app counts views. Updating the database on every view is too slow. How would you use a cache to count views, and what can go wrong?
- On a post, the comment count shows 120, then 118 on refresh, then 121. Users think comments are being deleted. What causes this and how do you fix it cheaply?
- A cricket highlight gets 50,000 likes per second. How consistent does the like count need to be, and how do you build it?
- Your likes table in DynamoDB is partitioned by post_id. When a celebrity posts, that one partition gets throttled while the table overall is mostly idle. How do you fix it?
- You tripled the number of app servers, but total throughput only went up 20%, and p99 latency got worse. How do you investigate?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.