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
Update a player's score when they finish a match (add points or set a new best).
Show the global top N (e.g. top 100).
Show a player's own rank and the players just above and below them.
Leaderboards per period (daily, weekly, all-time) and per region/friends.
Out of scope: anti-cheat (mention), matchmaking.
Non-Functional Requirements
Near real time: rank changes visible within a second or two.
Low latency reads of top N and own rank (tens of ms).
Scale: 50M players, 5M daily active, ~10K score updates/s at peak; top-100 reads far more frequent.
Scores are durable (a crash must not reset the season).
Core Entities
Player, Leaderboard (id per game + period + region).
ScoreEvent: player_id, match_id, points, timestamp (the durable source of truth).
Entry: (leaderboard_id, player_id) → score, in the ranking structure.
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)
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)
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
One sorted set per period (lb:g1:day:2026-10-01, ...:week:2026-w40, ...:all); expire old period keys after archiving them.
Ties: encode a tiebreaker into the score, e.g. score * 10^10 + (MAX_TS - achieved_at) so earlier achievers rank higher (keep within the 53-bit exact-integer range of a double).
Friends leaderboards: fetch the friends' scores (ZMSCORE) and sort the few hundred in the app; no separate structure per player.
Scaling beyond one node
Top N across shards: shard players by id across K sorted sets; each shard returns its top N and the service merges K×N entries.
Exact global rank across shards needs "how many players score above X on each shard" (ZCOUNT per shard, summed), still O(K log N).
For enormous boards, approximate ranks for most players are acceptable ("top 12%"): bucket scores into a histogram and compute a percentile, with exact ranks only for the top tier.
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.