System Design Lab
A YouTube Like Counter is not a blind INCR. Store user-video reaction state as the source of truth, then let a single counter, sharded counters, or an async projection serve public counts at different scales.
Adjust reaction rows, write and count-read QPS, hot-video skew, client retries, counter shards, cache hit rate, and freshness. See why Toggle is not idempotent, when one counter row becomes hot, and when the public count should become a replayable asynchronous projection.
Guided walkthrough
Reason about it one step at a time
Takeaway
Normal evolution scenarios
Click left to right for the intended demo path. Each card changes the workload inputs.
Recommended shape
Bottlenecks
Why this changes
Decision tradeoffs
Source-backed rules
These are the durable system-design claims behind the model. The exact slider thresholds are deliberately labeled as teaching assumptions.
A composite primary key enforces one row per user-video pair
PostgreSQL primary keys can span columns and automatically create a unique B-tree index. (video_id, user_id) expresses identity and protects one current state under concurrency.
PostgreSQL — ConstraintsINSERT ... ON CONFLICT provides a deterministic upsert path
Reaction writes can target the composite unique key, then combine the upsert with a transaction, version, or row lock to apply a previous-to-desired transition.
PostgreSQL — INSERTRow-level locks block concurrent writers to the same row
That can serialize one reaction or counter row, and also explains why a viral video’s single counter row becomes a hot lock.
PostgreSQL — Explicit LockingRedis INCR is an O(1) atomic counter primitive, but it does not know prior business state
INCR atomically increments an integer. It does not know whether a request is a retry, whether the user already liked the video, or which two deltas a LIKE-to-DISLIKE switch requires.
Redis — INCRThe same event key in one Kafka partition preserves order
Using (video_id, user_id) as the event key lets consumers observe that user’s transitions in write order. The system does not need expensive global ordering.
Apache Kafka — IntroductionOutbox commits business state and a publishable event together
The reaction row and outbox row commit in one transaction. CDC / Relay publishes later, avoiding a synchronous dual write where the database succeeds and the message is permanently lost.
Debezium — Outbox Event RouterTeaching assumptions
- All throughput, latency, and storage formulas expose architectural inflection points; they are not PostgreSQL, Redis, Kafka, or cloud-vendor performance promises.
- The teaching budget is 1k writes/s for one counter row and 2.5k writes/s per counter shard. Real boundaries require row-lock wait, p95, and skew load tests.
- Reaction truth uses roughly 112 bytes per active row including the row, primary key, and basic storage amplification. Schema, compression, and replicas change this.
- The async projection starts at 500ms of lag; a three-second count-cache TTL adds to public visible staleness.
- A desired-state API makes network retries idempotent no-ops. The underlying transaction/version still chooses a concurrent winner.
- The same user-video key maps to a stable counter shard and event partition. Public counts may be eventually consistent; the user’s own reaction always reads the source of truth.
- Reconciliation can rebuild aggregates from UserVideoReaction. A count that cannot be rebuilt has been incorrectly treated as a source of truth.