Real-Time Gaming Leaderboard System Design Interview Guide
"Design a real-time leaderboard" sounds simple until you say the word "rank" out loud and an interviewer asks what it means under concurrent updates, ties, corrections, and a season boundary. This page treats FlowRank — the leaderboard for the fictional game Arena Clash — as the running example: a specific score policy, a specific tie-break rule, and a specific set of named players (Ava, Ben, Chloe, Dev, Eli) whose exact scores and ranks you can reason about out loud instead of hand-waving. You'll get the opening clarifying questions, a realistic 40-45 minute walkthrough, model answers at core and staff depth, whiteboard exercises, a strong-vs-weak comparison, a 30+ question bank, and a quick-revision summary.
Clarify the Problem
"Design a leaderboard" is underspecified in a way that's easy to miss because everyone has seen a leaderboard before. A top-100 widget on a marketing page, a per-match scoreboard, and a global real-time competitive ranking system are three very different systems that all render as a sorted list of names and numbers.
The first few minutes should separate "designing a leaderboard" from "designing gameplay or matchmaking." A leaderboard system ingests already-decided outcomes — a verified score, a completed run, a match result — and ranks them; it does not decide who wins a match, compute collision damage, or run anti-cheat heuristics beyond trusting (or not trusting) the score it's handed. Losing this boundary early is a common failure mode: candidates start describing game-server tick rates and matchmaking pools when the actual system under design is a ranking and real-time-delivery problem.
- Player count and concurrency — how many total players, and how many are actively playing and submitting scores at once? This changes whether a single-node sorted structure is even plausible.
- Number of games/modes/scopes in play — is this one leaderboard, or many (per-game, per-mode, per-region, per-season, friends-only, global)? FlowRank's running example is Arena Clash, mode solo, season S7, scope global — stated explicitly, not assumed.
- Score policy — is a higher score always better (highest-score mode), do scores accumulate over time (cumulative mode), or does a lower value win (fastest-time mode)? These are three genuinely different comparators, not one universal "bigger is better" rule.
- Tie-breaking policy — what happens when two players have the identical score? FlowRank's rule: highest verified score wins; ties broken by earlier achievement timestamp, then stable player ID ascending — producing deterministic unique ordering (1, 2, 3, ...), not competition ranking (1, 1, 3) or dense ranking (1, 1, 2).
- Leaderboard types needed — global top-N, a specific player's rank, rank-around-a-player (a window around one player), and friends-only — each has a different access pattern and a different naive-implementation trap.
- Rank freshness requirement — does rank need to update within milliseconds of a score event ("real-time"), or is a bounded staleness window (seconds) acceptable? Naming an explicit, bounded staleness contract beats an unstated assumption of instant consistency.
- Read vs write rate — leaderboard reads (viewing rank, viewing top-N) are typically far more frequent than score-submission writes; stating this skew up front justifies caching and read-optimized structures later.
- Season rollover — does ranking reset periodically (seasons), and what happens to a score event that arrives late, against a season that has already closed?
- Cheating and score integrity — are submitted scores trusted as-is, or do they need server-side verification/anti-cheat validation before they can affect rank? This is a real architectural fork, not a footnote.
- Exact-rank requirement — does the interviewer want an exact global rank for every player (expensive at scale) or is an approximate/bucketed rank acceptable once player counts get large?
- Friends list / social scope — is a friends-only ranked view required, and where does the friends list itself come from (a separate social graph service, assumed to exist)?
- Regional scope and availability expectations — is this a single global ranking, or regional boards that roll up to a global one, and what's the acceptable availability/consistency trade-off if a region is partitioned from the rest?
“I'll scope this as FlowRank, the leaderboard system for a game called Arena Clash — specifically its solo mode, season S7, with a single global ranking as the primary case, plus top-N, a player's own rank, rank-around-a-player, and a friends-only view as required leaderboard types. I'll assume scores arrive from a trusted game-server event pipeline that has already verified them, so I don't need to re-derive anti-cheat logic inside the leaderboard itself — just handle duplicate and out-of-order events correctly. Ties are broken by earliest achievement timestamp, then stable player ID, so ranking is always a deterministic total order, never a tie in the displayed rank. I'll also assume reads (viewing a leaderboard) outnumber writes (score submissions) by a wide margin, and that a few hundred milliseconds of rank staleness is an acceptable, explicitly stated trade-off rather than claiming true zero-latency consistency. Does that match what you want me to dig into?”
The 40-45 Minute Walkthrough
This is a pacing guide, not a script to recite verbatim. A strong interviewer will branch into whatever they want to probe first — follow them there, and return to the rest only if time remains.
| Time | Focus | What to cover |
|---|---|---|
| 0-5 min | Scope and semantics | Scope FlowRank/Arena Clash solo/S7/global as the running example; define score policy (highest-score as baseline, name cumulative and fastest-time as distinct modes); state the deterministic tie-break rule (timestamp then player ID) up front since it underlies every later example. |
| 5-10 min | Requirements, API shape, rough sizing | Functional requirements (submit score, get top-N, get my rank, get rank-around-me, get friends ranking); a rough illustrative read/write skew assumption stated explicitly; the handful of invariants you'll defend all interview (unique deterministic ordering, idempotent score application, no silently lost or duplicated events). |
| 10-20 min | Baseline design: DB, Redis, query patterns | Start with the naive relational approach (a scores table, ORDER BY score DESC for top-N), say explicitly where it breaks under real read volume and real-time rank queries, then introduce a Redis sorted set keyed by player with score as the sort key, and walk ZADD/ZREVRANK/ZREVRANGE against the original five-player board. |
| 20-30 min | Authoritative events, idempotency, projection, live updates | Introduce an authoritative score-event log as the source of truth, idempotent application keyed by event ID (Test C/D), a projection step that updates the sorted-set view from that log, and a push mechanism (SSE/WebSocket) that notifies clients of rank changes rather than making them poll. |
| 30-40 min | Hot boards, sharding, failure, seasons, trade-offs | Discuss a very large/hot global board (partitioning options and why they complicate exact rank), what happens when the projection or cache layer fails, season rollover semantics (Test H), and the exact-vs-approximate-rank trade-off at scale. |
| Remaining time | Interviewer follow-ups and recap | Whatever the interviewer digs into — reconnect/resync protocol, moderation corrections, multi-region writes. Treat these as the actual signal of the interview and close with a short recap of the architecture and its explicit trade-offs. |
Resist drawing this full diagram in minute two. The value of the walkthrough is watching you arrive at this shape because of specific problems you named yourself — the "ORDER BY on every page view doesn't scale" problem motivates the sorted-set cache, the "duplicate network retry" problem motivates idempotent event IDs, the "client polling every second is wasteful" problem motivates push delivery. An interviewer who sees the final diagram at minute two can't tell whether you understand it or memorized it.
Core Questions With Model Reasoning
These are the questions almost every real-time leaderboard interview touches, at roughly mid-level depth. Each answer angle is a reasoning path, not a script to memorize — the mechanism detail lives in Architecture Deep Dive; link there rather than re-deriving every structure live.
How would you fetch the top-N players?
With the original FlowRank board — Ava 9,800 (#1), Ben 9,500 (#2), Chloe 9,500 (#3), Dev 9,100 (#4), Eli 8,700 (#5) — top-N is a single range query against a structure that's already sorted by the full ranking key (score, then tie-break fields), not a per-request ORDER BY scan over the full player table. A Redis sorted set with ZREVRANGE is the canonical mechanism, returning the top N members in O(log N + M) where M is the number requested.
- What if two players have the exact same score — does ZREVRANGE alone break the tie correctly? — Not by itself — a raw Redis sorted set breaks ties by member name lexically, which isn't FlowRank's rule. The real score stored in the sorted set has to encode the full tie-break key (score, then inverted timestamp, then player ID) into one comparable value so the structure's native ordering already matches the product's deterministic rule.
How would you fetch a single player's rank?
A direct rank lookup by player ID, not a scan of the whole board — ZREVRANK against the sorted set returns the 0-indexed position directly. Using Test A's post-event board (Ava #1, Dev #2, Ben #3, Chloe #4, Eli #5), looking up Dev's rank should return exactly #2 in O(log N), independent of total player count.
- Does this requirement change if you need an exact global rank across many shards? — Yes — a single-node sorted set gives an exact rank for free; once the board is sharded for scale, exact global rank requires merging per-shard counts, which is a materially harder problem covered in the staff-level section.
How would you implement rank-around-a-player (a window around one player)?
First get the player's rank via ZREVRANK, then fetch an inclusive window of rank - radius to rank + radius via ZREVRANGE. Test F: from the original board, Chloe (radius 1) returns Ben (#2), Chloe (#3), Dev (#4) — a 3-player inclusive window centered on Chloe.
- What happens if the player is near the very top or bottom of the board? — The window simply clamps — a radius-1 request for Ava (#1) returns only Ava and Ben, since there's no rank #0; the API contract should explicitly define this as a clamped, not an error, response.
How would you implement a friends-only leaderboard?
You cannot filter a global top-100 result and hope friends appear in it — most friends won't be in the global top-100 at all. The correct approach is fetching each friend's score directly (by player ID) and sorting that specific subset server-side. Test E: Ava's friends are Ben, Dev, Eli; from the original board, the friends-only ranking among just those three is Ben #1 (9,500), Dev #2 (9,100), Eli #3 (8,700) — explicitly a friends-subset rank, not their global rank. Ben's global rank is #2, but his friends-rank here is #1.
- Where does the friends list itself come from? — A separate social-graph service, assumed to already exist and be queried by the leaderboard service — the leaderboard doesn't own friend relationships, it only consumes a friend-ID list to drive a multi-get-and-sort operation.
- Does this scale if a player has thousands of friends? — A multi-get of thousands of scores followed by an in-memory sort is still cheap relative to a full-board scan, but it's a real cost that should be named rather than assumed away for an unusually social player.
How exactly are ties broken, and why does it matter?
FlowRank's policy: highest verified score wins; ties broken by earlier achievement timestamp, then stable player ID ascending. This produces a deterministic unique ordering — every player gets a distinct rank number (1, 2, 3, ...) — never competition ranking (1, 1, 3, where tied players share a rank and a gap appears after) and never dense ranking (1, 1, 2, no gap). Test B makes this concrete: from the original state, Eli reaches 9,500 at 10:06:00. Ben stays #2 (9,500 @10:01), Chloe stays #3 (9,500 @10:02), Eli is #4 (9,500 @10:06 — the latest of the three ties), and Dev drops to #5 (9,100, now behind all three tied 9,500 scores). All three of Ben, Chloe and Eli have the exact same score and are ordered purely by achievement timestamp.
- Why not just use whichever score event arrived at the server first? — Arrival order at the server can be affected by network jitter and retries; the achievement timestamp (when the score was actually earned, assigned by the trusted game-server pipeline) is the fair, deterministic tiebreaker — not receipt order.
- What if two players achieve the identical score at the identical timestamp? — That's exactly why player ID is the third-level tiebreaker — it guarantees total determinism even in a timestamp-collision edge case, since no two players share an ID.
Why Redis sorted sets (ZADD/ZREVRANK) specifically, and what are their real limits?
A sorted set keeps all members ordered by score with O(log N) insertion, rank lookup, and range queries — exactly the operation profile a leaderboard needs for top-N, player-rank, and rank-around-player. The real limit: a sorted set lives entirely in memory on (practically) one node's data structure, so a board with hundreds of millions of members has real memory-footprint and single-node-throughput implications, and tie-breaking requires encoding the full comparator into the stored score rather than relying on the raw score field alone.
- Is Redis the source of truth, or a derived view? — A derived, fast read-and-rank view — the authoritative record is the score event, and the sorted set is rebuildable from it; losing the sorted set is an availability problem, not a data-loss problem, as long as the event log survives.
What's the difference between highest-score, cumulative, and fastest-time modes?
These are three distinct comparators, not one universal rule. Highest-score mode keeps a player's best verified score and compares higher-is-better. Cumulative mode (Test J) sums event values over time: three events arrive — +100, +50, and a duplicate of the +50 event (same event ID) — the correct total is 150, not 200, because duplicate detection by event ID prevents double-counting; this is idempotent summation, not a "keep the higher value" comparison, which is a highest-score-mode-only rule. Fastest-time mode (Test K) ranks ascending — a lower completion time is better and ranks higher — with its own explicit tie policy; this is not the same comparator direction as highest-score mode, and assuming one universal "bigger number wins" rule across modes is a real bug.
- Can one player appear on leaderboards in multiple modes at once? — Yes — each mode/scope combination (e.g. Arena Clash solo highest-score global, vs. a separate speedrun fastest-time board) is its own independent ranked structure with its own comparator; they don't share state.
- Does idempotent summation in cumulative mode ever need a 'best attempt' semantics instead? — That would actually be a different, highest-single-value policy applied per event rather than a running sum — naming which one a given leaderboard mode uses is a required product decision, not something to assume.
How do you decide whether a score submission is trustworthy enough to affect rank?
The leaderboard system doesn't re-derive anti-cheat logic itself — it trusts a score event only if it arrives from the authoritative, already-verified game-server/anti-cheat pipeline, carrying a durable event ID and an achievement timestamp. A client directly POSTing an arbitrary score to the leaderboard API, bypassing that verified pipeline, should be rejected or routed through verification first — the leaderboard's correctness depends entirely on the integrity of what it's handed.
- What if verification is itself slow or asynchronous? — The leaderboard should either hold the score unranked until verification completes, or (if the product wants provisional display) clearly mark it as provisional — silently ranking an unverified score as if it were final is a correctness risk the design should name explicitly.
How do you handle a duplicate score event (the same event delivered twice)?
Every score event carries a unique, durable event ID; applying an event is idempotent on that ID — if the same event ID is seen again, it's a no-op. Test C: reprocessing evt-101 (Dev's verified 9,700 from Test A) a second time has zero additional effect — Dev's score and rank stay exactly as Test A left them (9,700, rank #2).
- Where does deduplication actually happen — producer or consumer side? — Consumer-side, at the event-application step — you can't fully rely on an upstream producer never retrying, so the projector itself must check "have I already applied this event ID" before mutating any ranked state.
How do you handle an out-of-order or delayed score update?
For highest-score mode, a new event only updates the stored best score if its value actually improves on the current best for that player — delayed delivery of an older, lower score is a safe no-op rather than an overwrite. Test D: after Test A (Dev's best is 9,700, rank #2), a late/delayed Dev event carrying his older 9,200 score arrives — it's rejected as a no-op because 9,200 is less than the current best of 9,700. Dev's best stays 9,700 and his rank stays #2; nothing is overwritten.
- Does this conditional-update rule apply to cumulative mode too? — No — cumulative mode doesn't compare values at all, it adds them exactly once per unique event ID (Test J); applying the highest-score-mode 'compare and keep higher' rule to cumulative mode would silently drop legitimate additions, which is a real cross-mode bug to avoid.
What happens if the same player submits two high scores at nearly the same instant from two different sessions?
Score application for one player must be processed with a single logical order (e.g. per-player event ordering, or a compare-and-set style conditional update keyed on current best), so two near-simultaneous updates for the same player don't race into an inconsistent final state. Whichever update is applied last must still respect the highest-score-mode comparison rule — the final stored value is always the true maximum of everything seen, regardless of arrival order.
- Does this require locking the player's record? — Not necessarily a traditional lock — an atomic conditional update ("set score to X only if X is greater than the current stored score") achieves the same correctness without a separate lock/unlock step, and is the more common real-world mechanism.
Why not just run ORDER BY score DESC LIMIT N against a relational database on every leaderboard view?
Leaderboard reads (viewing top-N, checking your own rank) happen far more often than score-submission writes, and a full or even indexed ORDER BY scan repeated on every page view doesn't scale to real-time, high-read-volume traffic — especially for rank-around-player and exact-rank queries, which need an efficient position lookup, not just a sorted page. The right split is a relational (or durable) store for the authoritative score history and reporting, plus an in-memory sorted structure (Redis sorted set or equivalent) maintained as a fast, continuously updated ranked view for the hot read path.
- So is the database still involved at read time at all? — Rarely on the hot path — it's the source of truth for score history, audits, and rebuilding the ranked view from scratch, not the thing directly answering every leaderboard page view.
How do you keep the leaderboard view 'live' without clients constantly polling?
A push mechanism — Server-Sent Events or WebSockets — notifies connected clients of relevant rank changes (their own rank moved, or a top-N change) as they happen, instead of every client re-polling on a fixed interval regardless of whether anything changed. The projector that updates the ranked view is also the natural place to emit these change notifications.
- Does every rank change need to be pushed to every connected client? — No — only to clients for whom it's relevant (their own board view, or a top-N they're currently watching); pushing every global mutation to every connection doesn't scale and isn't useful to most of them.
What happens when a client reconnects after being offline for a while?
The client doesn't get a full re-fetch by default, nor does it silently jump straight to the latest state — it requests a contiguous delta replay from its last-known board version forward. Test I: a client's last-applied board version is 42, and the server is currently at version 46 — the client requests versions 43 through 46 as a contiguous delta replay, not a full re-fetch of everything and not silently skipping straight to 46.
- What if the gap is too large to replay efficiently (client was offline a long time)? — The server can fall back to a fresh full snapshot plus the current version number when the delta gap exceeds a reasonable bound — but this is an explicit fallback decision, not the default behavior for an ordinary short reconnect.
How would you rebuild the ranked view if it were lost or corrupted?
Because the ranked view (sorted set) is a derived projection and the score-event log is the source of truth, rebuilding means replaying the event log in order, through the same idempotent, comparator-aware application logic, into a fresh structure — the same mechanism used for ordinary incremental updates, just run from the beginning (or from the last good snapshot).
- Does this rebuild need to pause live traffic? — Not necessarily if done against a separate structure that's swapped in once caught up (a blue-green rebuild) — pausing live reads/writes during a full rebuild is a simpler but more disruptive alternative, and which one you choose is a real trade-off to state explicitly.
A score is later found to be invalid (moderation/cheating) — how do you correct the leaderboard?
A correction is applied through the same event pipeline, as an explicit, audited event that invalidates or revises the prior score — never a silent in-place edit of the ranked view alone. Test G: from the original state, Ava's 9,800 score is invalidated by moderation; since she has no earlier valid score on record, she becomes fully unranked, not just demoted. The rest shift up exactly one position: Ben #1, Chloe #2, Dev #3, Eli #4.
- What if Ava did have an earlier valid score on record? — She'd fall back to that earlier score and be re-ranked accordingly, rather than being removed entirely — removal from the board only happens when there's no remaining valid score to rank her by.
How does season rollover work?
Test H: when season S7 is finalized, it becomes immutable, and a new, empty S8 opens for fresh ranking. A late-arriving S7 event must not be silently applied to either board — not to S7 (already finalized and closed to writes) and not to S8 (the wrong season entirely). If that event is ever allowed to count, it requires an explicit, audited correction process, not an automatic application to whichever season happens to be currently open.
- Why not just keep S7 open a little longer to absorb stragglers? — Because 'a little longer' isn't a bound — there needs to be a hard, stated cutoff after which S7 is truly closed, otherwise season-end payouts, snapshots, and rankings can never be treated as final with confidence.
Why might the leaderboard view be briefly stale, and is that acceptable?
Because the ranked view is a projection updated asynchronously from the authoritative event log, there's an inherent, bounded propagation delay between "score accepted" and "rank visibly updated." This is acceptable as an explicitly stated trade-off — a bounded staleness window, not unstated eventual consistency and not a false claim of zero-latency real time.
- What would make this unacceptable? — An unbounded or unmonitored staleness window — the design needs an observable lag metric and an alerting threshold, so 'a few hundred milliseconds, usually' doesn't silently become 'sometimes minutes' without anyone noticing.
How do you handle very high read traffic on a popular global board?
Top-N and rank-around-player reads are naturally cacheable for short windows since they change relatively slowly in aggregate even under high write volume — a short-TTL cache in front of the sorted-set reads, plus horizontally scaled read replicas of the ranked view, absorbs the bulk of read traffic without touching the authoritative write path.
- Does this caching conflict with the 'real-time' requirement? — Only if the cache TTL isn't chosen consistently with the already-stated bounded-staleness contract — the cache's staleness window should be a known, small addition to the existing projection lag, not a separate unstated delay.
Advanced Senior/Staff Follow-Ups
These are the questions that separate a candidate who can describe the architecture from one who has actually reasoned about its edges. Expect them in the last 10-15 minutes, or as immediate pushback on a core answer above.
Can you guarantee an exact global rank once the board is sharded across multiple nodes?
Not for free. A single-node sorted set gives exact rank trivially; once the board is partitioned (by hash of player ID, or by region), each shard only knows ranks within itself. An exact global rank requires either merging per-shard counts (how many players in every other shard score higher than this player — an aggregation across all shards on every rank query) or maintaining a separate, more expensive global-merge structure kept in sync with the shards — both add real latency or staleness cost compared to a single-node answer.
- Is there a cheaper alternative if exact rank isn't truly required? — An approximate rank (e.g. a percentile bucket, or a rank accurate within a known small error bound) is far cheaper to maintain across shards — this needs to be an explicit product trade-off, not a silently approximated answer presented as exact.
What happens with a very large, extremely hot global board (millions of concurrent score events)?
A single sorted-set node has a throughput and memory ceiling; beyond it, you need partitioning (by player-ID hash shard, or regional boards that roll up periodically), accepting that partitioning complicates exact global rank as discussed above. The honest framing: pick the partitioning scheme based on which query pattern (top-N, player rank, rank-around-player) you must keep fast and exact, and name which of those becomes approximate or higher-latency as a direct consequence.
- Does sharding by score range instead of player ID help? — It can make top-N trivial (the top shard holds the highest range) but creates severe hot-shard skew at the top of the distribution where most read traffic concentrates — a real trade-off to name, not a free win.
Where exactly is the write/ack durability boundary?
State it precisely: a score-submission write is acknowledged as accepted only after the event is durably recorded in the authoritative event log (not merely after the in-memory ranked view is updated) — so a crash after the ack can never lose the underlying fact that the score was accepted, even if the ranked view projection needs to catch up afterward.
- What's the trade-off of synchronous log-commit vs. a faster, less durable ack? — Synchronous commit-before-ack maximizes the durability guarantee at a small latency cost per submission; acking before durable commit is faster but risks a committed-to-the-client event that the system can't actually reproduce after a crash — rarely an acceptable trade for score integrity.
What happens if the projector crashes after committing the ranked-view update but before acknowledging the event as processed?
Be precise about what 'crash after commit' means: the data is durably committed — the ranked view already reflects the score, and the event log already has the event — even though whatever was waiting to be told 'processing complete' never received that confirmation. Recovery must redeliver the already-true outcome (confirm the event was in fact applied, by checking the idempotency record) rather than re-executing the projection step, which would risk a duplicate application if the idempotency check itself weren't trusted.
- Why is this fundamentally different from a lost event? — A lost event (never committed to the log at all) is genuinely absent from the system of record and needs to be resubmitted; a lost acknowledgment (committed, but confirmation never arrived) is already present and correct, and querying current state will show the true, already-applied outcome — conflating the two leads to incorrect, duplicate-prone retry logic.
Can you change the score comparator (e.g. from highest-score to cumulative) mid-season?
Not transparently — a live comparator change mid-season silently redefines what every existing ranked position means, which is effectively a correctness break for anyone who already has a rank under the old rule. The safe path is to treat a comparator change as a new leaderboard (or a new season) rather than mutating the live ranking rule of an in-progress board, and to communicate it as a deliberate, versioned change.
- What if the business genuinely needs this mid-season for a live event? — Then it should be modeled as a distinct, newly-created leaderboard scope running in parallel, not a redefinition of the existing one — preserving the old board's historical meaning intact.
How do you safely apply a historical result correction without corrupting the ranked view?
A correction event must be explicit, auditable, and applied through the same idempotent event-application path as any other event — referencing exactly which prior event it corrects or invalidates, rather than an ad hoc direct mutation of a player's stored score. Test G is this exact scenario: Ava's score is invalidated, and the resulting re-rank (Ben #1, Chloe #2, Dev #3, Eli #4) is a direct, traceable consequence of that one correction event, not a hand-edited board.
- Does this requirement extend to cumulative-mode corrections? — Yes, and it's trickier there — reversing a prior cumulative addition means emitting a compensating negative adjustment event (also idempotent on its own event ID), not simply recomputing the sum from a hand-edited history.
How does winner payout gating interact with correction timing?
A season's final standings (and anything gated on them, like payouts or rewards) should only be finalized after an explicit grace/moderation window during which late corrections can still be applied — finalizing and paying out immediately at season-close, then trying to claw back a payout after a correction surfaces, is a far worse failure mode than a brief, clearly communicated delay before finalization.
- What's a concrete failure if this grace window is skipped? — A cheater's inflated score could gate a legitimate reward decision (prize money, exclusive items) before moderation has a chance to invalidate it — the cost of being wrong here is much higher than the cost of a short, deliberate delay.
How would you handle multi-region write authority for a single global leaderboard?
Allowing every region to accept and apply score events locally and independently risks the same player's score being updated inconsistently across regions before replication catches up — the safer default is a single authoritative write path (or region) for a given global board, with other regions as fast read replicas, accepting the latency cost of cross-region writes as an explicit trade-off rather than claiming multi-region active-active correctness for free.
- Does this conflict with wanting low write latency for players in every region? — Yes, directly — this is the real trade-off to name: either accept higher write latency for non-authoritative regions, or accept the complexity (and correctness risk) of genuine multi-region write reconciliation, which is a materially harder problem than this design defaults to solving.
What are the privacy/friend-eligibility boundaries for the friends-only view?
A friends-only ranking must only ever be computed from a friend list the requesting player is actually entitled to see, and it must not leak a non-friend's score or existence through the ranking response — the leaderboard service should treat the social-graph service's friend list as the sole source of eligibility, and never attempt its own inference (e.g. 'players who share a region') as a substitute.
- Could a rank-around-player query accidentally leak a non-friend's score? — Only if rank-around-player and friends-only are implemented as the same code path without an explicit eligibility filter — they must be kept conceptually distinct: one windows the global board, the other filters to an explicit friend-ID set.
Is it safe to reprocess old events from the log (e.g. during a rebuild)?
Yes, as long as every event application is genuinely idempotent on its unique event ID and every comparator respects the stated mode's rule (conditional max for highest-score, additive-once for cumulative) — reprocessing the entire historical log from scratch should always converge to the identical final ranked state as the original incremental processing did, which is exactly what Test C demonstrates at a single-event scale.
- What would make reprocessing unsafe? — Any non-idempotent side effect tied to event application — e.g. sending a 'new high score!' notification as a direct side effect of the same code path that updates rank — reprocessing would then re-trigger notifications that already happened, which is why side effects like that should be decoupled from the core ranking state transition.
How do you reason about Redis memory cost for a sorted-set-backed leaderboard at scale?
Memory cost scales with the number of ranked members, not with read volume — so the real planning question is player count per board (and how many distinct board scopes exist: per-season, per-mode, per-region), not request rate. A design should name this as the actual capacity driver and state an illustrative assumption for it explicitly, rather than treating Redis memory as unlimited or ignoring it entirely.
- Does season rollover help bound this? — Yes — since each season's board is a bounded, eventually-archivable structure rather than an ever-growing single board, season rollover is also a practical memory-management mechanism, not just a product/ranking reset.
How does clock skew affect score-timestamp correctness and tie-breaking?
The achievement timestamp used for tie-breaking should come from a trusted, consistent source (the game server or event pipeline's clock), not from an untrusted client clock — if timestamps aren't reasonably synchronized across the servers producing them, two legitimately near-simultaneous achievements could be tie-broken in a way that doesn't reflect true order. This needs an explicit, stated assumption about timestamp trustworthiness and synchronization, not a silent assumption that all timestamps are perfectly comparable.
- Could you use a logical/sequence counter instead of wall-clock time for the tiebreaker? — That's a legitimate alternative worth naming — a server-assigned monotonic sequence number avoids clock-skew risk entirely, at the cost of the tiebreaker no longer directly reflecting real-world achievement time; which one FlowRank uses should be a stated, deliberate choice.
How do you protect tail latency (p99/p99.9) on leaderboard reads under load?
Name the actual hot-path stages (client, cache/read-replica, sorted-set lookup) and reason about where queueing delay grows nonlinearly as utilization approaches capacity — a credible answer identifies that uncached rank-around-player and friends-subset queries (which require a lookup plus a multi-get) are more expensive than a cached top-N read, and that headroom on the hottest shard, not just average throughput, is what protects tail latency.
- What's a concrete lever specifically for the friends-only path? — Bounding and batching the per-friend multi-get (rather than issuing one round trip per friend) keeps that path's own tail latency from dominating an otherwise fast global-board read path.
What's the fan-out cost of pushing live rank updates at scale?
Pushing every rank-affecting mutation to every potentially-interested connected client doesn't scale linearly forever — the fan-out cost should be bounded by only notifying clients actually watching a board region that changed (their own rank, or the specific top-N page they have open), and by batching/coalescing rapid successive updates rather than pushing every single intermediate mutation individually.
- Is there a risk of over-coalescing and feeling less 'real-time'? — Yes — this is the same bounded-staleness trade-off from the core section, just applied to push timing specifically; a small, stated coalescing window (e.g. batch updates every few hundred milliseconds) is a deliberate trade-off, not an accidental degradation.
How do you make the ranking process auditable end to end?
Every rank a player ever held should be reconstructable from the immutable event log plus the deterministic comparator and tie-break rules — not just observable as 'whatever the live sorted set currently shows.' This means the event log, not the live ranked view, is the actual audit source of truth, the same pattern used for correction events (Test G) and reprocessing (Test C).
- Why can't the live sorted set alone serve as the audit trail? — It only reflects current state — a player who was later corrected out of a rank, or whose season closed and rolled over, no longer has their historical position visible in the live structure at all; only the event log preserves that history.
How would you migrate from the naive relational-only design to the full event-log-plus-sorted-set architecture with zero rank regression?
Run both systems in parallel: build the new event log and sorted-set projection from the existing relational data as a one-time backfill, verify the resulting ranked view exactly matches the old system's output for a representative sample (including every tie-break case), then cut reads over to the new system only after that verification passes — never a hard cutover based on 'it looks right' alone.
- What's the single highest-risk verification case to get right? — The tie-break cases specifically (Test B-style scenarios) — a migration that gets simple distinct-score ranks right but subtly reorders tied players is exactly the kind of regression that's easy to miss in a casual spot-check and obvious the moment a real tie occurs in production.
Whiteboard and Pseudocode Exercises
These are hands-on exercises an interviewer might actually ask you to do at the whiteboard, not just talk about. Practice drawing and narrating each one in under 3-4 minutes.
1. Draw the board before and after Test A and Test B
Start from the original FlowRank board — Ava 9,800 (#1), Ben 9,500 (#2), Chloe 9,500 (#3), Dev 9,100 (#4), Eli 8,700 (#5). Draw it, then draw the post-state for Test A (Dev submits verified 9,700 at evt-101, 10:05:00): Ava 9,800 (#1), Dev 9,700 (#2), Ben 9,500 (#3), Chloe 9,500 (#4), Eli 8,700 (#5). From a fresh reset of the original board, draw Test B (Eli reaches 9,500 at 10:06:00): Ava 9,800 (#1), Ben 9,500 (#2), Chloe 9,500 (#3), Eli 9,500 (#4), Dev 9,100 (#5) — and narrate explicitly that Ben, Chloe and Eli all share the identical 9,500 score and are ordered purely by achievement timestamp.
2. Implement the best-score conditional-update pseudocode
function applyHighestScoreEvent(event):
if hasProcessed(event.eventId):
return // Test C: duplicate event, no-op
current = getStoredBest(event.playerId)
if current == null or event.score > current.score:
setStoredBest(event.playerId, event.score, event.achievedAt)
updateSortedSet(event.playerId, encodeRankKey(event.score, event.achievedAt, event.playerId))
// else: Test D -- event.score <= current.score, reject as no-op,
// current best and rank are left completely unchanged
markProcessed(event.eventId)3. Implement an idempotent cumulative increment
function applyCumulativeEvent(event):
if hasProcessed(event.eventId):
return // duplicate +50 event: no-op, total stays correct
incrementStoredTotal(event.playerId, event.delta)
updateSortedSet(event.playerId, getStoredTotal(event.playerId))
markProcessed(event.eventId)
// Test J: +100, +50, duplicate +50 (same eventId) -> total is 150, not 200.
// This is idempotent summation keyed on unique event IDs, never a
// "keep the higher value" comparison -- that rule only applies to
// highest-score mode.4. Implement the deterministic tie comparator
function compare(playerA, playerB):
if playerA.score != playerB.score:
return playerA.score > playerB.score ? A_FIRST : B_FIRST
if playerA.achievedAt != playerB.achievedAt:
return playerA.achievedAt < playerB.achievedAt ? A_FIRST : B_FIRST // earlier wins
return playerA.playerId < playerB.playerId ? A_FIRST : B_FIRST // ascending ID
// Verify against Test B: Ben(9500,@10:01) < Chloe(9500,@10:02) < Eli(9500,@10:06)
// all rank strictly above Dev(9100) -- a total, gap-free order: #2,#3,#4,#5.5. Explain Redis sorted-set operations and their real limits
Name ZADD (insert/update a member's score, O(log N)), ZREVRANK (get a member's 0-indexed rank in descending order, O(log N)), and ZREVRANGE (get a range of members by rank position, O(log N + M)). Then name the real limits out loud: the raw score field is a single number, so the full tie-break comparator has to be encoded into that one sortable value (e.g. packing score, an inverted timestamp, and player ID into one composite numeric key); and a sorted set's capacity and throughput are bounded by a single logical structure, which is exactly why very large boards need partitioning (and why partitioning complicates exact global rank, covered in the staff-level section).
6. Implement rank-around-player with snapshot/version semantics
Describe the shape: resolve the player's current rank via ZREVRANK, then fetch the inclusive window from rank - radius to rank + radius via ZREVRANGE, tagging the response with the ranked view's current version number (e.g. version 46). A client caching this result should treat it as valid only for that version; on the next push notification or reconnect, it compares versions and requests only the delta (Test I: client at version 42, server at 46, request a contiguous replay of 43 through 46) rather than re-fetching the whole window from scratch.
7. Explain precisely why filtering a global top-100 cannot produce a correct friends ranking
Walk through Test E concretely: Ava's friends are Ben, Dev and Eli. In the original global board, Dev is #4 and Eli is #5 — neither would appear in even a generous global top-10 on a board with many more players than this five-player example, let alone a realistic global board with millions of players. Filtering a global top-100 result down to 'which of these are my friends' would silently miss Dev and Eli entirely and present an incomplete, wrong friends ranking (perhaps showing only Ben). The correct approach fetches each friend's score directly by ID, regardless of their global rank, and sorts that exact subset — producing the correct friends-only order of Ben #1, Dev #2, Eli #3.
8. Design a versioned SSE/WebSocket resync protocol
Sketch the protocol: the server tags every pushed update with a strictly increasing board version number; a connected client tracks the last version it successfully applied; on reconnect, the client sends its last-known version, and the server responds with a contiguous replay of every delta since that version (Test I's 43-46 example) or, if the gap is too large to replay efficiently, a fresh full snapshot plus the current version number. Narrate explicitly why silently jumping straight to the latest version without replaying the gap would hide intermediate rank changes a client might need to show (e.g. 'you passed Chloe' notifications).
9. Draw the recovery path from event log to rebuilt rank index
Sketch the timeline: the projector has applied events up to version 1,204 when it's lost; a fresh rebuild loads the last verified snapshot (say, version 1,100), then replays journaled events 1,101 through 1,204 deterministically through the same idempotent logic used for live traffic, verifying the resulting board (e.g. spot-checking known tie cases) before it's swapped in to serve live reads.
10. Explain why merging shard-local ranks is insufficient for an exact global rank
Present the trap: naively asking each shard "what's this player's rank within you" and summing those local ranks does not produce a correct global rank, because each shard's local rank already accounts for only the players physically on it — it has no visibility into how many players on other shards also outscore this one. The correct approach is counting, per shard, how many players on that shard outrank the target player, summing those counts across every shard, and adding one — which requires touching every shard on every exact-rank query, the real cost this trade-off is about.
11. Design a safe season-close procedure under consumer lag
Walk through the sequence: stop accepting new S7-scoped events at a declared cutoff, wait out a stated grace window long enough to let any lagging consumers/event pipelines fully drain their already-in-flight S7 events, snapshot and freeze S7's final ranked state only once lag is confirmed at zero, then open S8 as a fresh, empty board. Narrate explicitly why closing S7 before confirming consumer lag has drained risks silently losing legitimate, already-submitted S7 events that simply hadn't been processed yet (a different failure than Test H's genuinely late event, but one the same grace-window discipline protects against).
Strong vs Weak Answers
The difference between a passing and failing answer at this level is rarely raw knowledge — it's whether the reasoning is anchored to explicit semantics and invariants, or floats on assumptions presented as facts.
| Recurring trap | Weak framing | Strong framing |
|---|---|---|
| Ranking semantics | Says "sort players by score" without ever naming the tie-break rule. | States the exact three-level comparator (score, then achievement timestamp, then player ID) and shows it produces a deterministic unique rank, never a shared competition rank. |
| Source of truth | Treats the Redis sorted set as if it were the authoritative data store. | Names the event log as the source of truth and the sorted set as a derived, rebuildable projection — losing the sorted set is an availability problem, not a data-loss problem. |
| Deduplication | Claims a vague "exactly-once" guarantee for the whole pipeline. | Names the actual mechanism — a unique event ID checked before mutation — and demonstrates it with Test C (reprocessing evt-101 has zero additional effect) rather than asserting it abstractly. |
| Technology reflex | "Just put it in Redis" or "just use Kafka" with no justification tied to the actual requirement. | Reasons from the access pattern first (O(log N) rank lookup and range queries) and only then evaluates whether a given structure or broker actually satisfies it, including its real limits (single-node ceiling, tie-break encoding). |
| Complexity claims | Claims exact global rank at massive scale is trivial and free. | States explicitly that exact global rank gets materially harder once the board is sharded, and names the actual mechanism (cross-shard count merge) and its cost rather than hand-waving it away. |
| Real-time claims | Claims the leaderboard updates with zero latency, instantly, everywhere. | States an explicit, bounded staleness contract (a small, monitored propagation delay from event commit to visible rank update) and treats that bound as a design requirement, not an embarrassing admission. |
| Ties | Ignores ties entirely, or says "ties are rare, don't worry about it." | Treats ties as a first-class, frequently-occurring case (Test B) and verifies the comparator against it explicitly rather than assuming distinct scores. |
| Moderation | Says corrections "just update the database row." | Describes a real moderation workflow: an explicit, audited correction event through the same idempotent pipeline, with Test G's exact before/after re-rank as the worked example. |
Interview Question Bank
At least 30 distinct questions across all sections of this page, grouped by theme. Sections below add new questions beyond Sections 3 and 4 rather than rephrasing them — each has a real answer outline and at least one follow-up.
Fundamentals
What's the actual difference between a leaderboard and a generic sorted list?
A leaderboard is a sorted list with a specific, product-defined comparator (not just numeric ordering), a stable tie-break rule, and a requirement to answer rank-position queries efficiently (top-N, a specific player's rank, a window around a player) — not merely to render a sorted page once.
- Does that mean a leaderboard always needs a specialized data structure? — Not at small scale — a simple sorted query can suffice; the specialized structure becomes necessary once read volume or rank-query latency requirements exceed what a plain sorted scan can deliver.
What's the difference between a per-match scoreboard and a persistent global leaderboard?
A per-match scoreboard is scoped to one short-lived match and typically discarded or archived after it ends; a persistent global leaderboard (like FlowRank's season S7 global board) aggregates across many matches/sessions over an extended period and must handle season rollover, corrections, and long-term storage concerns a per-match scoreboard never faces.
- Can the same underlying event stream feed both? — Yes — a single verified-score event can simultaneously update a per-match scoreboard and contribute to the persistent global leaderboard; they're two different projections of the same source events, not two separate systems collecting data independently.
Why does FlowRank scope its ranking as 'Arena Clash, solo, season S7, global' instead of just 'the leaderboard'?
Because every one of those dimensions (game, mode, season, scope) defines a logically separate ranked structure with its own player set and its own comparator state — collapsing them into one undifferentiated 'the leaderboard' would silently mix unrelated rankings together, which is a correctness bug, not just an organizational nicety.
- How many of these dimensions does a real product typically need simultaneously? — Often several at once (e.g. multiple modes, a rolling season, a global and a regional board) — the architecture should treat scope as a first-class key on every stored score and every query, not an implicit global assumption.
Data Structures
What's the time complexity of inserting a new score into the ranked view?
O(log N) for a balanced-structure-backed sorted set (N = number of ranked members) to both locate the correct position and update it — independent of where in the ranking the new score lands, which is exactly why this beats a full re-sort on every write.
- Does a batch of simultaneous score updates change this complexity? — Each individual update is still O(log N); a batch of K updates is O(K log N) if applied one at a time, though some implementations can offer batched-insert optimizations worth naming if asked.
Why not store rank as a precomputed integer field directly on each player's row?
Because a single score change anywhere in the board can shift every other player's rank below it by one position — precomputing and storing rank as a static field would require updating potentially many other rows on every write, which is far more expensive than deriving rank on read from an already-ordered structure.
- Is there any case where caching a computed rank is still worthwhile? — Yes — for a short-TTL cached response to a specific, repeated rank query (e.g. a popular player's own rank), as long as it's clearly a cache of a point-in-time answer, not a source-of-truth field that must always be correct.
How would you represent the fastest-time mode's comparator in a sorted-set-style structure?
Store the raw completion time as the sort key directly, since the structure's native ascending order already matches 'lower time ranks higher' — the opposite of highest-score mode, which needs its sort key's natural direction inverted or its ordering queried in reverse to express 'higher score ranks higher.'
- Could using the same stored-value convention for both modes cause a bug? — Yes — a projector or query layer that doesn't explicitly branch its comparison/ordering logic per mode could apply highest-score-mode's descending convention to a fastest-time board by mistake, inverting the entire ranking silently.
Correctness
What invariant must hold true after every single event is applied, regardless of mode?
Every ranked player has a distinct, gap-free integer rank (1, 2, 3, ...) with no two players sharing a rank and no rank skipped — the comparator and tie-break rule together must produce a strict total order at all times, which is the invariant every mode (highest-score, cumulative, fastest-time) must independently satisfy.
- How would you test this invariant automatically? — A property-based test that applies a random sequence of valid events (including same-score, same-timestamp edge cases) and asserts the resulting rank sequence is always a strict, gap-free permutation with no duplicate ranks.
Can applying events out of their true causal order ever produce a different final state than applying them in order?
For highest-score and cumulative modes with proper idempotent, order-independent application rules (conditional max; additive-once-per-ID), the final state converges identically regardless of application order — this is a deliberately designed property, not an accident, and it's exactly what makes safe reprocessing (Test C) and recovery possible.
- Does this property hold for a hypothetical mode where only the *most recent* score counts, regardless of value? — No — a 'most recent wins' comparator is explicitly order-dependent, so applying events out of true timestamp order would produce a different, wrong final state; this is a deliberate counter-example worth naming to show the convergence property isn't automatic for every possible mode.
Distributed Processing
What ordering guarantee does the score-event pipeline actually need to provide?
Strict global ordering across all players isn't required — each mode's correctness only depends on per-player ordering being eventually resolved correctly via the comparator (conditional max, additive, etc.), not on a single global sequence number across the entire event stream; this is a deliberately weaker, more scalable requirement than, say, a matching engine's strict per-book ordering.
- Does this mean partitioning the event stream by player ID is safe? — Yes — since correctness is per-player, partitioning the event pipeline by player ID (so one player's events are processed in a consistent relative order) is a safe and natural scaling approach.
How would you detect that the projector has fallen behind the authoritative event log?
Track the gap between the latest committed event sequence/version in the log and the last version the projector has successfully applied and published — an explicit, monitored lag metric, not an assumption that the projector is always caught up.
- What should happen automatically once lag exceeds a threshold? — Alerting at minimum, and potentially a read-path signal (e.g. serving slightly-stale-flagged responses) so clients and the season-close procedure can both react to a known degraded state rather than silently trusting a lagging view.
Caching and Real-Time Delivery
Should the push-update channel and the read-query path share the same underlying data access, or be separate?
They should read from the same underlying ranked-view projection (so pushed updates and subsequently-queried state are always consistent with each other), but the delivery mechanisms are different — one is a fan-out notification system, the other is a request/response query — and conflating the two into one code path risks coupling push-delivery failures to read-path availability.
- What's a concrete symptom of getting this coupling wrong? — A slow or overloaded push-fan-out layer causing ordinary top-N or rank queries to also slow down or fail, when the two should be able to degrade independently.
How would you decide the coalescing window for batched push updates?
As a direct extension of the already-stated bounded-staleness contract — the coalescing window should be a known, small fraction of (or equal to) that staleness bound, so batching updates doesn't silently introduce a separate, unaccounted-for delay on top of the one already promised.
- Should the coalescing window be the same for a top-N feed and a single player's own-rank feed? — Not necessarily — a player's own rank crossing a meaningful boundary (e.g. entering the top 10) might justify a tighter, more immediate push than a less time-sensitive general top-100 display, which is a legitimate product-level distinction to raise.
Scaling
What's the actual bottleneck as player count grows from thousands to hundreds of millions?
Memory footprint and single-node throughput of the ranked-view structure, not the event log itself (which is naturally append-only and horizontally scalable) — this is why partitioning strategy for the ranked view (not the event pipeline) is the central scaling decision at very large player counts.
- Does sharding the ranked view also help write throughput, or only read/memory? — Both, to a point — write throughput for a given shard is still bounded by that shard's own capacity, so sharding spreads write load across shards the same way it spreads read/memory load, as long as writes are reasonably distributed across player IDs rather than concentrated.
How would regional leaderboards roll up into one global board?
Each region maintains its own authoritative local ranked view for low-latency regional reads, while a separate aggregation process periodically (or continuously, with accepted lag) merges regional data into a global view — this is a deliberate, bounded-staleness roll-up, not a claim that the global view is always instantaneously consistent with every region.
- What happens to a player who transfers regions mid-season? — Their historical regional scores need an explicit, stated policy — carried forward into the new region's local view, kept separately, or excluded — rather than an implicit assumption about what 'moving region' means for ranking.
Seasons and Security
Should season boundaries be enforced client-side, server-side, or both?
Server-side, authoritatively — the server is the only party that can be trusted to reject a late S7 event or correctly route a fresh event to S8; a client-side season check is, at best, a UX nicety and must never be relied on for correctness.
- What's a concrete exploit if this were trusted client-side? — A manipulated or outdated client could submit an S7-scoped event after the season has closed and claim it should still count — the server's authoritative season-state check is what actually prevents this, not client cooperation.
How would you prevent a compromised or spoofed client from submitting an inflated score directly?
By never accepting a raw client-submitted score as ranking-eligible at all — only scores that have passed through the trusted, server-side verified game-server/anti-cheat pipeline (carrying its own event ID and achievement timestamp) are eligible to affect rank, which structurally removes the client's ability to just assert an arbitrary number.
- Does this fully eliminate cheating risk? — No — it shifts the trust boundary to the verification pipeline itself, which needs its own integrity guarantees; the leaderboard's job is to correctly apply whatever that pipeline hands it, not to be the last line of defense against cheating.
Staff-Level Judgment
If given unlimited budget, would you still keep the event-log-plus-projection architecture rather than writing directly to the ranked view?
Yes — this is a correctness-and-auditability-driven choice, not a cost-driven one; unlimited budget buys more shards, more replicas, and lower projection lag, but it doesn't remove the need for an authoritative, replayable record that corrections, audits, and recovery all depend on. A strong answer separates 'what more money buys' from 'what the correctness/auditability requirement demands.'
- What would unlimited budget actually change, then? — Lower projection lag (more projector capacity), more aggressive caching and regional replication, and more generous exact-rank guarantees at larger shard counts — all performance/availability levers, not a different correctness model.
How would you decide when it's time to move from a single-node ranked view to a sharded one?
Watch for the single-node structure's memory footprint or write/read throughput approaching its practical ceiling under real, measured traffic — not a guessed player-count threshold decided in advance — and treat the migration itself as requiring the same zero-regression verification discipline described in the staff-level migration question in Section 4.
- What's the actual signal that would trigger this change? — Sustained elevated p99 latency on rank queries or memory pressure alerts on the node holding the ranked view, even when no single player or key is behaving unusually — that's the tell that the bottleneck is the single structure itself, not a hot key.
Quick Revision
A condensed pass for the night before (or the hour before) the interview — the architecture shape, the invariants, the key Redis operations, and a 30-second and 2-minute answer you can actually deliver out loud.
The 30-second answer
FlowRank ingests already-verified score events from Arena Clash into an authoritative, append-only event log, applies each event idempotently (by unique event ID) according to the leaderboard mode's comparator — highest-score with conditional-max, cumulative with additive summation, or fastest-time with ascending comparison — into a fast, continuously-updated ranked view (a Redis sorted set keyed by a composite score-plus-tiebreak value), and serves top-N, player-rank, rank-around-player, and friends-only queries from that view, pushing changes to connected clients within a stated, bounded staleness window.
The 2-minute answer
I'd scope this as FlowRank for Arena Clash — solo mode, season S7, global scope — ranking already-verified score events, distinct from the game server or anti-cheat pipeline that decided those scores were real. Ranking uses a deterministic three-level comparator: score first, then earlier achievement timestamp, then stable player ID ascending, which guarantees a gap-free, unique rank for every player even under ties, as demonstrated by Test B. The authoritative record is an append-only score-event log, not the ranked view itself; every event carries a unique event ID so reprocessing or duplicate delivery is a safe no-op, demonstrated by Test C. A projector applies each event through the mode-appropriate comparator — conditional max for highest-score, which correctly rejects a late, lower-valued Dev update in Test D — into a Redis sorted set whose stored key encodes the full tie-break comparator, giving O(log N) top-N, player-rank, and rank-around-player queries via ZREVRANGE and ZREVRANK. Friends-only ranking is never derived by filtering a global top-N; it's built by fetching each friend's score directly and sorting that subset, as Test E shows concretely. Live updates are pushed to clients via SSE/WebSocket rather than polling, with a versioned protocol so a reconnecting client replays a contiguous range of missed deltas, demonstrated by Test I's version-42-to-46 example, rather than silently skipping ahead. Corrections (Test G) and season rollovers (Test H) both go through the same auditable, idempotent event pipeline rather than direct mutation. At scale, exact global rank gets materially harder once the ranked view is sharded, since per-shard ranks can't simply be merged — that's an explicit, named trade-off rather than a hand-waved 'it still works.'
Architecture-flow recap: client submits a run → trusted game-server/anti-cheat pipeline verifies and emits a score event with a unique event ID and achievement timestamp → the event is durably committed to the authoritative log before acknowledgment → an idempotent projector applies the event through the mode's comparator into the live ranked view → the ranked view serves top-N, player-rank, rank-around-player, and friends-subset reads → a versioned push channel notifies connected clients of relevant changes → clients reconnecting after a gap replay a contiguous delta range rather than re-fetching everything.
- Every ranked player has a distinct, gap-free rank — never a shared competition rank and never a dense-ranking gap, due to the deterministic score-then-timestamp-then-player-ID comparator.
- The event log, not the live ranked view, is the authoritative source of truth; the ranked view is a rebuildable projection.
- Every event has a unique, durable event ID; applying the same event ID twice is always a safe no-op (Test C).
- In highest-score mode, a new event only updates a player's best score if it's strictly an improvement — a lower or equal delayed value is rejected as a no-op (Test D), never silently overwriting a better existing value.
- Cumulative mode sums values exactly once per unique event ID — duplicate delivery never inflates the total (Test J).
- Fastest-time mode ranks ascending (lower is better) — never assume one universal 'bigger number wins' rule across modes (Test K).
- A correction is an explicit, audited event through the same pipeline, never a silent direct edit of a player's stored score or rank (Test G).
- A finalized season is immutable; late events against it require an explicit, audited correction process, never silent application to the closed season or the new one (Test H).
- Filtering a global top-N can never correctly produce a friends-only ranking — friends' scores must be fetched directly by ID (Test E).
- A reconnecting client replays a contiguous range of missed updates by version number — it never silently skips ahead and never re-fetches everything by default (Test I).
ZADD board:s7:global <encodedScore> <playerId> // insert/update a player's ranked position
ZREVRANK board:s7:global <playerId> // get a player's 0-indexed rank, descending
ZREVRANGE board:s7:global 0 99 WITHSCORES // get the top 100 with their encoded scores
ZREVRANGE board:s7:global <rank-radius> <rank+radius> WITHSCORES // rank-around-player window
ZMSCORE board:s7:global <id1> <id2> <id3> // multi-get friend scores for friends-only ranking| Trade-off | Choice made here | Alternative |
|---|---|---|
| Ranked-view storage | In-memory sorted structure (Redis-style) as a derived projection | Relational ORDER BY as the live read path |
| Score integrity | Trust only verified events from the game-server pipeline | Accept raw client-submitted scores directly |
| Exact vs approximate global rank | Exact on a single-node board; explicitly approximate once sharded | Always exact, at the cost of a cross-shard merge on every query |
| Live delivery | Push (SSE/WebSocket) with versioned resync | Client polling on a fixed interval |
| Season handling | Hard, audited cutoff with a grace window before finalizing | Keep the season open indefinitely to avoid edge cases |
| Failure scenario | Key response |
|---|---|
| Ranked-view projection crashes or is corrupted | Replay the authoritative event log from the last verified snapshot through the same idempotent logic to rebuild it. |
| A score event is delivered twice | The unique event ID makes reapplication a safe no-op — never a duplicate effect (Test C). |
| A delayed, stale score event arrives after a better one was already applied | Conditional-max comparison rejects it as a no-op (Test D) — the current best is never overwritten by a worse value. |
| A client reconnects after being offline | Request a contiguous delta replay from its last-known version, not a full re-fetch and not a silent skip-ahead (Test I). |
| A late event arrives against an already-finalized season | Rejected from both the closed season and the new one by default; only an explicit, audited correction process can ever apply it (Test H). |
| Aspect | Exact rank | Approximate rank |
|---|---|---|
| Single-node ranked view | Free — ZREVRANK gives it directly | Not needed at this scale |
| Sharded ranked view | Requires a cross-shard count merge on every query | Can be estimated cheaply from a bucketed or sampled structure |
| Staleness | As fresh as the projection lag allows | May intentionally trade a wider error margin for lower latency/cost |
| Best fit | Top-N, a player's own precise rank when it matters (e.g. near a reward cutoff) | High-level 'you're roughly in the top 5%' style displays at very large scale |