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