File: realtime-gaming-leaderboard/leaderboard.py

Date: 2026-06-05

Time: 14:00

realtime-gaming-leaderboard/leaderboard.py

Purpose

This file implements a real-time gaming leaderboard — the kind you'd design in a system design interview when asked "How would you build a leaderboard that supports millions of players with instant rank lookups?" It owns all core leaderboard operations: score updates, rank lookups, top-K/bottom-K queries, neighborhood queries ("around me"), score range queries, percentile computation, and per-player score history.

The implementation is a single-node, in-memory solution. It doesn't address distribution, persistence, or replication — those are architectural concerns above this layer. What it does demonstrate is the right data structure choice and API surface for the problem.

Key Components

Leaderboard

The core class. Maintains three parallel data structures that must stay in sync:

| Field | Type | Role |

|-------|------|------|

| entries | SortedList of (-score, timestamp, playerid) | Ordered sequence for rank operations |

| _players | dict[str, (score, timestamp)] | O(1) lookup of current score/timestamp by player ID |

| _history | defaultdict[str, deque] | Bounded score-change log per player |

Key methods and their contracts:

LeaderboardManager

Thin registry of named Leaderboard instances. Supports the multi-board pattern (daily, weekly, seasonal leaderboards). No cross-board operations.

Patterns

Negated-score trick

The central design choice. SortedList sorts ascending, but leaderboards rank descending. Storing (-score, timestamp, playerid) makes the highest score sort first. The timestamp as the second tuple element is a tiebreaker — equal scores are ordered by who got there first (lower timestamp = higher rank). The playerid as the third element ensures uniqueness even with identical score+timestamp.

Dual-index pattern

entries gives O(log n) ordered operations; players gives O(1) point lookups. Every mutation must update both. This is the classic tradeoff: you pay double the write cost to get fast reads on both access patterns.

Update-by-remove-and-reinsert

update_score doesn't mutate entries in place. It removes the old entry from the sorted list, then inserts a new one. This is the correct approach with sorted containers — in-place mutation would violate sort order.

Bounded history via deque(maxlen=...)

Score history is capped by history_size (default 10) using deque's built-in eviction. No manual size management needed.

Dependencies

Imports:

Imported by:

Flow

A typical write path through update_score:

1. Check if player exists in _players dict (O(1))

2. If yes, look up old (score, ts) and remove (-oldscore, oldts, playerid) from entries (O(log n))

3. Insert new (-score, timestamp, playerid) into entries (O(log n))

4. Update players[playerid] to (score, timestamp) (O(1))

5. Append to history[playerid] (O(1), deque handles eviction)

6. Compute rank via _entries.index(entry) (O(log n))

7. Return result dict

A typical read path through get_rank:

1. Look up (score, ts) from _players (O(1))

2. Reconstruct the exact tuple (-score, ts, player_id)

3. Call _entries.index(tuple) (O(log n))

4. Add 1 for 1-based ranking

Invariants

Error Handling

Minimal, by design. This is a data-structure-level implementation, not a service layer.

Topics to Explore

Beliefs