Game Leaderboard System Low-Level Design

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

  1. POST /scores {player_id, game_id, score}
  2. Validate: score not negative, not lower than existing (no score regression)
  3. Persist to DB: INSERT … ON CONFLICT DO UPDATE SET score = GREATEST(score, excluded.score)
  4. Update Redis: ZADD leaderboard:{game_id} {score} {player_id}
  5. 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.

Scroll to Top