System Design Interview: Design a Real-Time Leaderboard (Top-K System)

System Design Interview: Design a Real-Time Leaderboard (Top-K System)

Real-time leaderboards rank users by score and are common in gaming, ride-sharing, e-commerce, and social apps. The core challenge is maintaining a sorted ranking for millions of users with high update throughput. Asked at Snap, Doordash, Lyft, Coinbase, and gaming companies.

Requirements Clarification

Functional Requirements

  • Update a user’s score in real-time
  • Get the global top-K users (leaderboard)
  • Get a specific user’s rank
  • Get users near a specific user’s rank (surrounding players)
  • Support time-windowed leaderboards (daily, weekly, all-time)

Non-Functional Requirements

  • Scale: 10M users, 100K score updates/second
  • Leaderboard read latency: <10ms
  • Rank accuracy: exact for top-1000, approximate acceptable for lower ranks
  • Real-time: leaderboard updates visible within 1-2 seconds

Core Data Structure: Redis Sorted Set

Redis sorted sets (ZSET) are the perfect data structure for leaderboards:

# Add or update user score (O(log N))
ZADD leaderboard:global {score} {user_id}

# Get top-K users (O(log N + K))
ZREVRANGE leaderboard:global 0 K-1 WITHSCORES

# Get user rank (0-indexed, O(log N))
ZREVRANK leaderboard:global {user_id}

# Get user score
ZSCORE leaderboard:global {user_id}

# Get users around rank R (surrounding players)
ZREVRANGE leaderboard:global R-5 R+5 WITHSCORES

# Increment score atomically (O(log N))
ZINCRBY leaderboard:global {delta} {user_id}

Redis sorted sets use a skip list internally, giving O(log N) for all rank operations. For 10M users: all operations run in single-digit milliseconds.

Architecture

Score Event (game action, purchase, etc.)
    |
Score Service API
    |
Kafka (score_updated events)
    |
Score Processor (Flink/Consumer)
    - Validate and deduplicate events
    - Update Redis ZSET: ZINCRBY leaderboard {delta} {user_id}
    - Update persistent DB (Cassandra/Postgres)
    |
Leaderboard API (read path)
    - ZREVRANGE for top-K (served from Redis)
    - ZREVRANK for user rank (served from Redis)
    - Cache hydrated user profiles (Redis hash or Memcached)

Time-Windowed Leaderboards

Daily, weekly, and all-time leaderboards require separate sorted sets:

# Separate ZSET per time window
ZADD leaderboard:daily:{YYYYMMDD} {score} {user_id}
ZADD leaderboard:weekly:{YYYY-WW} {score} {user_id}
ZADD leaderboard:global {cumulative_score} {user_id}

# Expire daily leaderboards automatically
EXPIRE leaderboard:daily:20260415 86400  # 24h TTL

# Score events update all relevant windows
ZINCRBY leaderboard:daily:{today} {delta} {user_id}
ZINCRBY leaderboard:weekly:{this_week} {delta} {user_id}
ZINCRBY leaderboard:global {delta} {user_id}

Scale Considerations

Redis Memory

Redis ZSET for 10M users: approximately 80-100 bytes per member = 800MB-1GB. Fits in a single Redis instance. For 100M users: partition by user ID range or game region across multiple Redis instances.

Sharding Strategy

If a single Redis cannot handle the load:

  • Global top-K: each shard maintains a local top-K. Merge top-K results from each shard to compute global top-K. Merge is cheap (K * num_shards candidates).
  • User rank: user assigned to shard by user_id % N. Rank within shard + count of higher-scored users in other shards = global rank (requires cross-shard count query, done infrequently or cached).

Approximate Rank for Non-Top Users

For users ranked 100,000th+, exact rank is less important. Use approximation: sample N users from sorted set, count how many have higher score, extrapolate. Or segment by score buckets and count per bucket.

Persistent Storage

Redis is the serving layer; Postgres or Cassandra is the source of truth:

user_scores: (user_id, score, last_updated_at)
score_events: (user_id, event_type, delta, timestamp)

# Redis is rebuilt from DB on Redis restart
# Score events written to DB for audit and replay

Leaderboard Hydration

Top-K query returns user IDs and scores. Hydrate with user profiles (username, avatar, country):

# Batch fetch user profiles
user_ids = ZREVRANGE leaderboard:global 0 99 WITHSCORES
profiles = HMGET users {user_id1} {user_id2} ... {user_id100}
# User profiles cached in Redis hash with TTL

Interview Tips

  • Redis sorted set is the key insight – know ZADD, ZREVRANK, ZREVRANGE, ZINCRBY
  • Explain time-windowed leaderboards as separate ZSETs with TTL
  • Discuss sharding for top-K: merge local top-K from each shard
  • Know that ZINCRBY is atomic – no race conditions on concurrent score updates
  • Mention persistent DB for crash recovery and audit
  • Profile hydration via batched HMGET, not N individual lookups

Scroll to Top