← System Design
Large-Scale Products

Design a Real-Time Leaderboard

Watch: "Top-K System Design Interview Breakdown w/ Ex-Meta Senior Manager" — Hello Interview
In simple words

A scoreboard that keeps players always sorted, like a stack of cards ordered by score. Adding points just slides one card to its new position, so "top 100" and "what's my rank?" are read straight off the stack.

Problem Statement

Design a real-time leaderboard for an online game or contest: players earn points, and anyone can see the top 100 players and their own rank, updated within seconds. Ranking millions of players by score on every update sounds expensive; the right data structure makes it cheap.

Functional Requirements

Non-Functional Requirements

Core Entities

API Design

POST /v1/scores { player_id, match_id, points }   (idempotent on match_id)  -> 202
GET  /v1/leaderboards/{id}/top?n=100               -> [{ rank, player, score }]
GET  /v1/leaderboards/{id}/players/{pid}?around=5  -> { rank, score, neighbours[] }

High-Level Design (HLD)

Leaderboard with a sorted setGame serversPlayers(top 100, myrank)Score APIScore events(source oftruth)Score consumerRedis sorted setZINCRBY /ZREVRANKmatchresultread
Durable events feed an in-memory sorted set that answers top-N and rank queries in O(log N).
game servers -> Score API -> ScoreEvents (DB / log, idempotent by match_id)
                                   |
                           score consumer
                                   |  ZINCRBY lb:{game}:{period} points player
                                   v
                        [ Redis sorted sets ] (replicated)
                                   ^
clients -> Leaderboard API --------+  top N: ZREVRANGE ... WITHSCORES (cached ~1 s)
                                      my rank: ZREVRANK + ZREVRANGE around it
Periodic snapshot of each board -> DB (for history and recovery)

Low-Level Design (LLD)

Redis sorted set operationsScore eventZSET lb:g1:week40member = playerscore = pointsZREVRANGE 0 99→ top 100ZREVRANK p42→ my rankZREVRANGE r−5 r+5→ neighboursZINCRBY
Every operation is O(log N): updates, top-N and rank all read the same always-sorted structure.

Why a sorted set

Relational: SELECT COUNT(*) + 1 FROM scores WHERE score > :my_score
            -> an index range scan over everyone above you: slow at millions,
               and it runs on every "my rank" request

Redis sorted set (skip list + hash):
  ZINCRBY lb:g1:2026-w40 50 "p42"         O(log N)   add points
  ZREVRANGE lb:g1:2026-w40 0 99 WITHSCORES O(log N + 100) top 100
  ZREVRANK lb:g1:2026-w40 "p42"            O(log N)   my rank
  ZREVRANGE lb:g1:2026-w40 r-5 r+5         O(log N + 10) neighbours

50M members with a member id and a score is a few GB in memory, which fits one large Redis node; replicate it for availability and rebuild from the durable score events if lost.

Periods, ties and regions

Scaling beyond one node

Follow-up Questions

The same match result is submitted twice. Does the player get double points?
No: score events are stored with a unique match_id (per player), and only newly inserted events are applied to the sorted set.
Redis loses the data. How do you recover?
Rebuild from the latest snapshot plus score events after the snapshot. The sorted set is a derived view; the event store is the source of truth.
Top-100 is requested 100K times per second. Is Redis enough?
Cache the top-100 response for ~1 s in the API tier or at the edge. Everyone sees the same list, so one Redis read per second serves all of them.

Related