Articles
Leaderboard System Design Interview: Ties, Seasons, and Rank Freshness
Prepare for leaderboard system design with stable ties, seasons, rank freshness, sharding, idempotent score updates, and rebuilds.
A leaderboard system design interview becomes interesting when “sort users by score” stops being enough. Two players can tie, a new season can open while the old one is still queried, and a client can retry the same score update. Users also ask for different views: top 100, my exact rank, ten neighbors around me, friends only, and historical winners.
Redis officially documents sorted sets as unique members ordered by score, with lexicographic member ordering when scores are equal. That default is deterministic, but it is not automatically the product's rank policy. Decide whether tied players share a displayed rank and whether earlier achievement, fewer attempts, or user ID breaks the tie. Carry that rule through writes, indexes, pagination, and APIs.
Leaderboard System Design Interview: Quick Answer
Store authoritative score state under (competition_id, season_id, player_id) and accept updates with a stable event_id or monotonic version. Build one serving index per hot scope, such as global, region, or league. Make the order key explicit: (score DESC, achieved_at ASC, player_id ASC) for unique positions, or group equal scores for competition or dense ranks.
Return a season version and as_of watermark with rank responses. Use a durable event log or database snapshot to rebuild in-memory indexes. Calendar seasons are separate immutable namespaces; rollover creates a new writable season and freezes the prior one after a stated late-event window. Sharding by player spreads updates but makes exact global rank a distributed count; sharding by score simplifies range queries but can hot-spot popular bands. Choose from the required queries.
Define Rank Semantics with a Tie Example
Given scores A=100, B=100, C=90, unique-position ranking can return A:1, B:2, C:3 using a stable secondary key. Competition ranking returns A:1, B:1, C:3; dense ranking returns A:1, B:1, C:2. All are defensible. The API must name one because “rank 2” means different things under each policy.
If time breaks ties, define which time: first time the final score was achieved, last update time, or season registration time. Updating updated_at on every retry can reorder tied players unfairly. Persist the achievement timestamp as part of authoritative state and make retries idempotent. For pagination, include the full order tuple and season version in the cursor. An offset alone can duplicate or skip players when scores change between pages.
Model Scores as State Derived from Events
Choose whether clients submit deltas, absolute match results, or authoritative totals. Blind +10 requests are easy to retry incorrectly. A safer game flow records a completed match with match_id, validates it, and derives the player's score once. The score table keeps the latest version; the event log supports audit and rebuild.
Separate score acceptance from rank materialization. A transaction commits the authoritative result and an outbox record. A consumer updates the serving index idempotently using the player version. If an older event arrives after a newer one, ignore it instead of overwriting the score. The API can return the accepted score version immediately and allow the rank endpoint to catch up to that version within the freshness objective.
Make Seasons First-Class Namespaces
Use an opaque season_id, not a query that guesses the current week from local time. Store opens_at, closes_at, timezone, scoring rules version, and late-finalization cutoff. At rollover, create and warm the new index before it becomes writable. Keep the closed season queryable under its own key; do not rename or clear the active sorted set in place.
Scores arriving near the boundary need an event-time rule. A match that started before close but finished after close may count in the old season only if the published contract says so. Record both event and receive times, and finalize after a bounded grace period. Once frozen, a correction creates a new season revision or an audited administrative change. Historical winner pages should reference that immutable revision so later rebuilds do not silently rewrite trophies.
Match Indexes to Rank Queries
A single Redis sorted set is strong for top ranges and one-member rank within a manageable scope. Friends-only ranks require intersecting social membership with score order; precomputing a set for every user can explode write amplification. Fetch a bounded friend list and score it on demand, or precompute only for high-traffic cohorts.
At global scale, player-hash shards balance score writes. Global top 100 can merge each shard's top 100 because an item outside a shard's top 100 cannot enter the global top 100. Exact rank of an arbitrary player is harder: each shard must count entries greater than the player's full order key. That fanout may be acceptable for rare queries, cached, or replaced with an approximate percentile. Say which endpoint receives which guarantee rather than promising one database operation for every view.
Attach Freshness to Every Read
“Real time” should become a number. Suppose accepted scores must appear in top lists within two seconds at p99, while historical pages may lag a minute. Each materializer publishes the latest durable offset or score version applied. The read service returns as_of and can wait briefly for a caller's min_version, fall back to authoritative score state, or report that rank is still updating.
Push notifications are hints, not the source of truth. A client reconnects with its last sequence, fetches the current page, and discards older updates. Monitor end-to-end age from authoritative commit to query visibility, not just broker lag. Also track rank-query latency, hot scopes, index memory, update rejection, and tie-policy violations found by offline checks.
Rebuild and Reconcile without Rank Flicker
Rebuild a lost serving index from an authoritative score snapshot at version V, then replay events after V. Write into a new generation, compare member count, score checksum, sampled ranks, and top-N against the old generation, then switch the read pointer atomically. A partial in-place refill can make users jump through meaningless ranks.
Reconciliation should compare player score and version, not only the top list. A missing low-ranked player may not affect top 100 today but will break rank(player) and future updates. Repair from authoritative absolute state rather than applying another delta. Keep anti-cheat validation before the durable score transition; removing a fraudulent score later is an audited correction with the same version and rebuild semantics as any other change.
Choose Exactness per Endpoint
Top-N, personal score, exact global rank, percentile, and neighbor pages should not inherit one blanket guarantee. Top-N can be exact within a slightly stale serving snapshot. Personal score can be read-after-write from authoritative state. Exact global rank can fan out across shards or use a specialized ordered index. Percentile can tolerate histograms or sketches if the product labels it approximate. Neighbors require a stable full-order cursor and are especially sensitive to concurrent movement.
Compare write amplification across scopes. Maintaining global, regional, clan, daily, weekly, and friends indexes on every event may turn one match into dozens of writes. Materialize the high-volume views, derive rare ones asynchronously, and retain the score facts needed to add a view later. For a friends board, on-demand sorting of hundreds of IDs may be simpler and cheaper than fanout-on-write to every follower.
State consistency choices in user terms. Eventual rank may be acceptable if the UI shows “updating” and never loses an accepted score. Tournament payouts may require a frozen, audited snapshot and block finalization until all partitions reach the cutoff watermark. The same leaderboard service can support both only if APIs distinguish provisional and final results.
Make the Answer Reviewable
Turn the leaderboard system design design into a small deterministic review before adding more scale. Write the initial state, five to ten timestamped inputs, every expected state transition, and the exact read result at two watermarks. Replay one input, deliver another out of order, pause one consumer, and rebuild the serving view from its durable boundary. The replay should not create an extra effect, the stale input should follow the declared version rule, and the rebuilt read should match the authoritative result through the same watermark.
Keep expected results independent of the implementation. If the same code produces both the output and the assertion, a shared assumption can hide the defect. Add one invariant check that scans all state, even if production serves a faster index. For a distributed design, say which part this fixture does not prove: it does not measure production latency, network partitions, or capacity. Load and fault tests come next, using the traffic and error budgets already stated.
Finish with an operational table: signal, healthy range, action, and user-visible consequence. Include ingestion or queue age, rejected or duplicate work, authoritative-versus-serving divergence, and tail read latency. This makes the proposal inspectable. An interviewer can now change one requirement and see exactly which state, test, and alert must change with it.
Separate what the review establishes from what it leaves open. A passing deterministic fixture proves the declared state transitions for those inputs. It does not establish performance at scale, fairness across real users, or behavior during every network failure. Add a capacity test for the estimated peak, a skew test concentrated on one key or partition, and a fault test at the acknowledgment boundary. Record versions and random seeds so failures can be replayed. When a result differs from expectation, reduce it to the smallest trace, name the violated invariant, and repair that transition before retuning capacity. This evidence-first sequence keeps correctness questions from being hidden inside infrastructure changes.
Keep the review artifact beside the implementation and date the environment that produced it. If a later optimization changes ordering, batching, numeric width, or retry behavior, rerun the same trace before accepting the new result. The invariant is the stable comparison point; the component diagram is not.
A 45-Minute Interview Structure
Spend five minutes on competition scope, rank policy, season rules, and required queries. Use eight minutes for APIs and authoritative score state. Draw the event/outbox/materializer/index path in ten. Spend ten on top-N, exact rank, neighbors, and sharding. Use seven for rollover, late events, and immutable finals. Reserve five for rebuild, anti-cheat correction, and freshness metrics. Keep one tie example visible throughout so every component implements the same order.
Final Takeaway
A leaderboard answer is strong when users would see stable, explainable results. Define ties and season identity first. Make score updates replay-safe and derive serving indexes from durable state. Give each endpoint an explicit exactness and freshness contract. Then show how a new season, a late event, or a lost index produces a controlled version transition instead of rank flicker.
Sources and Further Reading
- Redis sorted sets
- Redis leaderboard use case
- Redis leaderboard implementation example
- PostgreSQL window functions
Documentation supports the stated system or algorithm behavior; individual question records support only their own practice contracts. Architecture recommendations and interview sequencing are editorial inference.