Requirements
- Update a player score in real time
- Query the global top-100 leaderboard
- Query a player’s exact rank among all 100M players
- Query players ranked just above and below a given player
- 10K score updates per second
Core Data Structure: Redis Sorted Set
Redis sorted sets are purpose-built for leaderboards. All operations run in O(log N).
# Update player score (creates or updates atomically)
ZADD leaderboard:game1 {score} {player_id}
# Top 100 (highest scores first)
ZREVRANGE leaderboard:game1 0 99 WITHSCORES
# Player's 0-indexed rank (lower = better)
ZREVRANK leaderboard:game1 {player_id}
# Players around rank 50 (+/- 5)
ZREVRANGE leaderboard:game1 45 55 WITHSCORES
# Player's score
ZSCORE leaderboard:game1 {player_id}
Memory: ~100 bytes per member. 100M players = ~10GB RAM. Use Redis Cluster for horizontal scaling or a high-memory instance.
Data Model
Player(player_id, username, avatar_url, created_at)
Score(player_id, game_id, score, updated_at) -- persistent DB
Redis ZSET: leaderboard:{game_id} -- score -> player_id
Redis HASH: player:{player_id} -- username, avatar (for fast display)
Score Update Flow
- POST /scores {player_id, game_id, score}
- Validate: score not negative, not lower than existing (no score regression)
- Persist to DB: INSERT … ON CONFLICT DO UPDATE SET score = GREATEST(score, excluded.score)
- Update Redis: ZADD leaderboard:{game_id} {score} {player_id}
- Publish to Kafka for analytics and achievement triggers
GREATEST prevents score regression from out-of-order updates. ZADD updates atomically — no read-modify-write needed.
Time-Windowed Leaderboards
Separate sorted sets per time window: ZADD leaderboard:{game_id}:daily:{YYYYMMDD}. Set EXPIREAT for automatic cleanup after the display window. On each score update, write to both the all-time and current daily/weekly sorted sets. To query yesterday’s top-100: ZREVRANGE leaderboard:{game_id}:daily:{yesterday}.
Displaying the Leaderboard
Top-100 display: ZREVRANGE returns 100 player_ids. Pipeline 100 HGET player:{id} for username/avatar in one round trip. Cache the rendered JSON: key=lb_top100:{game_id}, TTL=5 seconds. Invalidate when any top-100 member’s score changes. For 100M players, the top-100 barely changes second-to-second, so 5s staleness is fine.
Friend Leaderboard
Fetch scores for all friend IDs using ZMSCORE leaderboard:{game_id} friend_id_1 friend_id_2 … (up to ~5000 friends). Sort the result client-side or in the application layer. Faster than maintaining a separate friend sorted set per user. For players with many friends (social games), compute offline and cache per user.
Key Design Decisions
- Redis ZSET is the canonical leaderboard structure — never use SQL ORDER BY on 100M rows
- GREATEST in DB prevents stale out-of-order updates from lowering a player’s score
- Separate ZSET keys per time window with EXPIREAT for daily/weekly boards
- Cache rendered top-N with short TTL to reduce Redis round trips under high read traffic
Snap system design covers real-time leaderboards and ranking. See common questions for Snap interview: leaderboard and ranking system design.
Twitter system design covers trending and ranking systems. Review design patterns for Twitter/X interview: leaderboard and trending system design.
Amazon system design covers leaderboards and real-time ranking. See design patterns for Amazon interview: game and ranking system design.