Courses › System Design › Question Breakdowns

Design a Leaderboard

Sorted sets for rank in O(log n), time-windowed boards, tie-breaking, sharding and approximate ranks.

Design a real-time leaderboard for an online game: players' scores update as they play, and anyone can see the global top 100, their own rank, and the players just above and below them. It's a question about ranking at scale — keeping a sorted structure with fast updates and fast rank queries — plus time-windowed boards, sharding and tie-breaking.

Requirements

Functional

  • A player's score updates when they finish a match
  • Anyone can see the top N (e.g. top 100) players
  • A player can see their own rank and the players around them (±5)
  • Boards exist per time window (daily, weekly, all-time) and optionally per region or friend group

Out of scope: matchmaking, anti-cheat (mention validating scores server-side), rewards distribution.

Non-functional

  • Scale: 100M players, ~10M daily active; ~10k score updates/second, peaks of ~50k
  • Real time: rank changes visible within a second or two
  • Fast reads: top-N and my-rank in < 50ms
  • Exact ranks for the top; approximate ranks are acceptable for players deep in the list (discuss)

[!TIP] Why a database ORDER BY isn't enough SELECT COUNT(*) FROM scores WHERE score > :mine gives an exact rank, but scans up to 100M rows per request. A B-tree index makes top-N fast, yet "what's my rank" remains a count over a huge range. The interview hinges on a structure that answers rank in O(log n): a sorted set (skip list with span counts).

Capacity estimates

One Redis node handles ~100k+ sorted-set ops/second, and 10 GB fits in memory — a single board can live on one node (with replicas). Many boards (daily × regions) spread across a cluster.

Core Entities

  • Player — ID, name, region
  • Score event — player, match ID, points, timestamp
  • Leaderboard — ID (e.g. global:weekly:2026-W41), window, sorted (player, score) entries

API

POST /v1/matches/{matchId}/results     (game server → us)
{ "results": [{ "playerId": "p1", "points": 30 }, ...] }

GET /v1/leaderboards/{boardId}/top?n=100
→ { "entries": [{ "rank": 1, "playerId": "p9", "score": 98410 }, ...] }

GET /v1/leaderboards/{boardId}/players/{playerId}?around=5
→ { "rank": 18233, "score": 4120, "neighbors": [...] }

Scores come from game servers, never directly from clients, so players can't post arbitrary scores.

High-Level Design

gs = client "Game Servers" @0,0
api = service "Score Service" @1,0
db = store "Scores DB\n(source of truth)" @2,0
kafka = queue "Score Events" @1,1
upd = service "Board Updater" @2,1
redis = cache "Redis Sorted Sets\n(one per board)" @3,1
read = service "Leaderboard API" @3,2
user = client "Players" @4,2
gs -> api : match results
api -> db : idempotent by matchId
api -> kafka
kafka -> upd
upd -> redis : ZINCRBY
user -> read
read -> redis : ZREVRANGE / ZREVRANK
  • Game servers report match results; the score service writes them to a durable scores DB (idempotent by match ID) and publishes events.
  • Board updaters apply each event to every relevant board (global:alltime, global:weekly:W41, eu:daily:2026-10-11) with ZINCRBY.
  • Reads hit Redis directly: ZREVRANGE board 0 99 WITHSCORES for the top 100, ZREVRANK board player for a rank, then a range around it.

Deep Dives

1. The sorted set

Redis sorted sets combine a hash map (member → score) with a skip list ordered by score, where each forward pointer stores how many elements it skips. That makes:

If Redis isn't allowed, the same idea is an order-statistic tree (a balanced BST with subtree sizes) in your own service.

2. Ties

Equal scores sort by member name in Redis, which is arbitrary. To rank ties by "who got there first", encode time into the score: score × 10^10 + (MAX_TS − achievedAt). A double has 53 bits of integer precision, so budget the bits (or use a composite member key) carefully.

3. Time windows

Create one sorted set per window instance: weekly:2026-W41. Each event updates the current daily, weekly and all-time boards. Old boards get a TTL (or are snapshotted to the DB for history) — no expensive "subtract last week's points" logic.

4. Scaling beyond one node

A single board of 100M fits one node; you'd shard only for write throughput or much bigger boards.

  • Shard by score range (0–1k, 1k–10k, …): a rank is "count of players in higher shards + rank within my shard". Keep a small table of shard sizes. Ranges must be rebalanced as scores grow.
  • Shard by player hash: writes spread evenly, but top-N needs a scatter-gather (top N from each shard, merged), and an exact global rank needs counts from every shard.

[!NOTE] Approximate ranks for the long tail Players at rank 18,233,412 don't need an exact number. Keep a histogram of score buckets and report "top 18%" or an estimated rank from the bucket counts. Exact ranks only for, say, the top 100k, which live in one sorted set.

5. Durability and recovery

Redis is the serving layer, not the source of truth. Score events are in the DB/Kafka, so a lost board can be rebuilt by replaying events (or reloading per-player totals) into a new sorted set. Run Redis with a replica and AOF for fast failover; rebuild is the backstop.

6. Friends and regional boards

Region boards are just more sorted sets updated from the same events. Friend leaderboards don't need their own sets: fetch the player's friends (hundreds), read their scores in one ZMSCORE, and sort in memory.

What's expected at each level

  • Mid-level: a working design where a cache-backed sorted structure serves top-N, with a durable store for scores.
  • Senior: Redis sorted sets and their complexities, rank-around-me, one set per time window, rebuilding from events, server-authoritative scores.
  • Staff: sharding strategies and their trade-offs for rank queries, approximate ranks for the tail, tie-breaking encodings, hot-board write rates and multi-region boards.