HomeBlog › Design a Leaderboard

Design a Leaderboard

A real-time ranking system is a favorite mid-level prompt. The full arc — requirements, the ranking problem, Redis sorted sets, top-K and rank queries, sharding, and ties.

"Design a leaderboard" looks trivial until you ask how a player learns they are ranked 4,812,377th out of ten million — instantly, while scores keep changing. That single requirement rules out the naive "store scores in a table and ORDER BY" answer and pushes you toward the right data structure. It is a clean, focused prompt that rewards knowing one thing deeply: the sorted set.

Here is a complete leaderboard system design walkthrough with an architecture diagram, following the standard requirements → API → design → data → scale arc.

   [ Game / App Client ]
       |            ^
   score event   top-K / my-rank
       v            |
   [ API / Leaderboard service ]
       |            |
     ZADD        ZREVRANGE / ZREVRANK
       v            |
   +---------------------------+
   |   Redis Sorted Set (ZSET) |
   |   member -> score, ranked |
   +---------------------------+
       |
       v  (async persistence)
   [ Durable store: SQL / KV ]  <-- source of truth, replay/rebuild
Leaderboard architecture: score writes ZADD into a Redis sorted set; reads use ZREVRANGE (top-K) and ZREVRANK (rank-of-user); a durable store is the source of truth

1. Clarify the requirements

Functional requirements

Non-functional requirements

Back-of-the-envelope scale: say 10M active players and 1M score updates/minute during peak (~17K writes/sec), with reads an order of magnitude higher. That fits comfortably in memory — 10M entries at ~100 bytes is about 1 GB — which is exactly why an in-memory sorted set is the natural fit.

2. API design

POST /score
  body: { "userId": "u123", "delta": 50 }   // or absolute score
  -> 200 { "score": 4200, "rank": 87 }

GET /leaderboard/top?k=100
  -> 200 [ { "userId": "...", "score": ..., "rank": 1 }, ... ]

GET /leaderboard/rank/{userId}
  -> 200 { "rank": 4812377, "score": 3300 }

3. The ranking problem

The whole question is really "how do I keep millions of items ordered by a score that changes constantly, and answer both top-K and what is X's rank cheaply?" Consider the obvious options:

ApproachUpdateRank-of-userVerdict
SQL table + ORDER BYO(1) writeO(N) count of higher scoresDies at scale
Cached sorted arrayO(N) insertO(log N) binary searchWrites too slow
Sorted set (skip list + hash)O(log N)O(log N)The answer

The SQL approach is fine for a small board but computing rank means counting every player with a higher score — a full scan per request. A sorted set gives you fast writes and fast rank reads at the same time.

4. Sorted sets (Redis ZSET)

The canonical implementation is a Redis sorted set (ZSET). Internally it is a skip list (keeps members ordered by score, supports O(log N) rank and range queries) paired with a hash map (member → score, for O(1) score lookup). That combination is what makes every operation you need cheap:

# update a score (increment)
ZADD board INCR 50 user:123        # O(log N)

# top-10, highest first
ZREVRANGE board 0 9 WITHSCORES     # O(log N + K)

# a specific player's rank (0-based, highest = 0)
ZREVRANK board user:123            # O(log N)

# players around a user (rank r): fetch r-2 .. r+2
ZREVRANGE board r-2 r+2 WITHSCORES

No sorting happens at query time — the set is always ordered, so top-K and rank-of-user both ride the existing structure. This one data structure answers every functional requirement, which is exactly why interviewers reach for this prompt.

Interview signal: naming the sorted set and explaining why it beats ORDER BY — O(log N) rank instead of an O(N) count — is the core insight the interviewer is listening for.

5. Ties and composite scores

When two players share a score, Redis orders them lexicographically by member key — stable but arbitrary. If ties need real meaning (say, whoever reached the score first ranks higher), fold the tiebreaker into the score itself. Pack the primary score into the high bits and an inverted timestamp into the low bits of a single number:

compositeScore = score * 1e13 + (MAX_TS - reachedAt)
# higher score wins; equal scores -> earlier timestamp wins

Now a single numeric sort key enforces both rules, and every ZSET operation still works unchanged.

6. Real-time updates and durability

Each score event is a single ZADD, so writes are cheap and the ranked view is always current. Delivery to clients is either polling the top-K every few seconds or pushing over WebSocket/pub-sub for a live board.

Redis is fast but in-memory, so the sorted set is a read/compute cache, not the source of truth. Persist raw score events asynchronously to a durable store (SQL or a KV store) so you can replay and rebuild the ZSET after a restart. Redis persistence (AOF/RDB) plus a replica covers most failure cases.

7. Scaling: sharding and approximate ranks

One Redis node handles a surprising amount, but past its limit or for isolation you shard. Two common strategies:

Exact global rank across shards is expensive, so large systems return approximate ranks for the long tail: each shard tracks local counts and you sum boundary offsets, accepting small error. Nobody notices whether they are ranked 4.81M or 4.82M — but the top-N is always exact, because that is the part everyone actually looks at.

Key trade-offs the interviewer probes

Framework reminder: every system design answer follows the same arc — requirements → estimates → API → high-level design → data model → scale → trade-offs. Keep the system design cheat sheet in mind and narrate which stage you're in.

Structure any design answer with live AI support

CoPilot Interview surfaces a structured design skeleton — requirements, API, data model, and scaling — in about 4 seconds during real Zoom and Teams calls. Free for Windows and macOS, invisible on screen-share.

Download free

FAQ

What data structure powers a leaderboard?

A sorted set. Redis ZSET is the canonical choice: each member (player) has a score, entries stay ordered by score, and updates, top-K reads, and rank lookups are all O(log N). It is essentially a skip list plus a hash map, giving fast ranked reads and fast score updates in one structure.

How do you get a player's rank efficiently?

With a Redis sorted set, ZREVRANK returns a member's rank directly in O(log N) — no full scan. ZREVRANGE returns the top-K in O(log N + K). Both operations use the sorted set's internal order, so you never sort the whole set at query time.

How do you scale a leaderboard beyond one machine?

Shard the sorted set by score range or by segment (region, tier). Exact global rank across shards is expensive, so large systems return approximate ranks — each shard knows its local counts and you sum boundary offsets — reserving exact ranking for the small top-N that everyone actually looks at.

How do you handle ties in a leaderboard?

Redis breaks ties lexicographically by member key, which is stable but arbitrary. For meaningful tie-breaking (earliest to reach the score), encode a composite score — for example score in the high bits and an inverted timestamp in the low bits — so a single numeric value sorts by both.

How are real-time leaderboard updates delivered?

On each score event the service does a ZADD to update the sorted set. Clients either poll the top-K every few seconds or subscribe over WebSocket/pub-sub for pushes. Because the write is a single O(log N) operation, updates propagate to reads almost immediately.