Design problems
Classic building blocksMediumSDE-2 30 min

Design a distributed counter for likes and views

Asked at
GoogleJioHotstarLinkedInMetaNetflixShareChat

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

likes as set membership with unique keyidempotent like/unlikepre-aggregation / batching writessharded counters for hot keysKafka + stream processing windowseffectively-once via idempotent sinkHyperLogLog for unique viewerseventual consistency of counts with optimistic UIbatch reconciliation of driftbot and duplicate filtering for views

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

View events~300k/s average, 1M/s peak500M DAU × ~50 views/day ≈ 25B views/day ÷ 86,400 ≈ 290k/s; ×3 peak during a big match or festival
Like events~25k/s average, 100k/s peak500M DAU × ~4 likes/day = 2B/day ÷ 86,400 ≈ 23k/s; ×4 peak
Like rows stored~730B rows/year, ~15-20 TB/year2B/day × 365; ~24 B per row (user_id, post_id, ts) before overhead; Cassandra overhead brings it to ~15-20 TB
Counter rows~10B items × ~32 B ≈ 320 GBpost_id + likes + views + updated_at; hot subset cached in Redis
DB writes after aggregation~20-50k/s1M views/s grouped per item per 5-10 s window: only items with activity get one write per window
Viral item~200k views/s on one videoA cricket highlight or celebrity post; must not map to one DB row or one Redis key updated per event

3 API

EndpointWhat it does
PUT/v1/posts/{id}/likeIdempotent like. Returns {liked: true, like_count}. Repeating it does nothing.
DELETE/v1/posts/{id}/likeIdempotent unlike.
POST/v1/events/viewsBatched 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 / ScyllaDB
partition (post_id)clustering (user_id)liked_at

Source 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 cache
item_id (PK)like_countview_countupdated_at

Written 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 info

Durable buffer; 3-day retention for replay. Partitioning by item_id lets one aggregator own each item's count.

unique_viewersRedis HyperLogLog / Druid sketches
key = uv:{item_id}:{date}~12 KB HLL

Estimates unique viewers with ~0.81% standard error; merges across days and shards.

daily_rollupsData warehouse (BigQuery/ClickHouse) + S3
item_iddateviewslikesunique_viewers

Exact batch recompute from raw events each night; used for creator analytics and to correct drift in the online counters.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAPUT likeview batchesGET countsupsert like rowlike delta eventview eventsbatched upsertsrefresh hot countsMGETon missraw events sinkcorrect driftMobile / web clientsAPI gatewayLike serviceCount read serviceView ingest serviceNightly reconciliationFlink aggregator(windows)Kafka: likes, viewsLikes table(Cassandra)Counts tableRedis counts + HLLWarehouse / S3 events
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

User likes a post

  1. 1Client calls PUT /v1/posts/p1/like and immediately shows the filled heart and count + 1 (optimistic UI).
  2. 2Like service writes (post_id=p1, user_id=u7) to the likes tables. If the row already existed, it returns success without emitting anything.
  3. 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.
  4. 4It publishes {event_id, post_id, delta: +1} to Kafka keyed by post_id.
  5. 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.
  6. 6Redis count for p1 is updated; other users see the new count within a few seconds.

Views of a viral video

  1. 1Clients send view events in batches every 10 s (fewer requests on slow 3G/4G networks in smaller cities).
  2. 2Ingest service validates, drops obvious bots (rate per device, known bad IPs), adds event_id and writes to Kafka keyed by item_id.
  3. 3A hot item is detected; the ingest layer spreads its events over 16 sub-keys (item#0..#15) so no single partition is overloaded.
  4. 4Flink keeps counts in state per key, de-duplicates by event_id within the window, and every 10 s emits a total per item.
  5. 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.
  6. 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

Q1How would you show 'Liked by Rahul and 1.2M others'?
Q2How do you handle a user deleting their account (remove their likes from counts)?
Q3How would you count views per country and per hour for creator analytics?
Q4How do you move this to multiple regions?
Q5What if counts must be exact for an ad billing contract?

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.

Start free