EN 中文

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

Normal evolution scenarios

Click left to right for the intended demo path. Each card changes the workload inputs.

Workload

These are inputs, not preset architecture stages.

Recommended shape

Current architecture path
YouTube Like Counter architecture Whiteboard-style Like Counter architecture: the Reaction Store keeps user-video truth; low traffic updates a counter synchronously, while high traffic uses Outbox, Event Log, and Aggregator to build a sharded aggregate. Count Cache serves only public reads. Clients APIs Source of truth Count projection Serving Viewer set + read reaction Reaction API idempotent set Count API serve aggregate Reaction store (video, user) state Outbox / CDC capture commits Partitioned log order by user-video Counter aggregator versioned transition Counter shards video + user hash Count cache short-lived snapshot
Clients
Viewer Set a desired reaction and read the public aggregate
APIs
Reaction API Serialize each user-video transition and return the user’s latest reaction
Count API Read a cache or aggregate counter shards and return an as-of timestamp
Source of truth
Reaction store Store reaction, version, and updated_at; a composite key prevents duplicate current state
Outbox / CDC Reliably hand committed reaction transitions to the asynchronous projection
Count projection
Partitioned log Preserve order for each user-video key and support retry, replay, and backpressure
Counter aggregator Deduplicate events and apply previous-to-desired transitions to materialized counts
Counter shards Spread hot-video writes; count reads sum shards or consume a snapshot
Serving
Count cache Cache only a derived aggregate; TTL, refresh, and pipeline lag all contribute to freshness

Bottlenecks

Retry correctness

Hottest counter-shard write pressure

Reaction-truth write partitions

Count backend read fan-out

Public-count freshness

Reaction-truth storage

Why this changes

    Decision tradeoffs

    Desired-state API

    User-video reaction truth

    Atomic state transition

    Per-video counter sharding

    Outbox + async projection

    Public count cache

    Per-key ordering / region ownership

    Reconciliation

    Source-backed rules

    These are the durable system-design claims behind the model. The exact slider thresholds are deliberately labeled as teaching assumptions.

    Verified rule

    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 — Constraints
    Verified rule

    INSERT ... 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 — INSERT
    Verified rule

    Row-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 Locking
    Verified rule

    Redis 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 — INCR
    Verified rule

    The 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 — Introduction
    Verified rule

    Outbox 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 Router

    Teaching 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.