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

Source: https://www.techinterview.org/post/3233463499/system-design-real-time-leaderboard/
Updated: 2026-04-17 · techinterview.org

## 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
