1. Full mock interview#
This is an end-to-end example of a well-structured 45-minute system design interview for a senior backend engineering role. Read it as a reference for what good answers sound like, how transitions work, and how the candidate controls pacing.
Functional requirements 0:00 – 3:30
Interviewer
Design a leaderboard system for a large-scale multiplayer mobile game with a global player base. Take it wherever you think is most important — feel free to ask me any clarifying questions before you start.
Candidate
Thanks. Before I design, a few questions to make sure I'm solving the right problem. First — what leaderboard scopes do we need? Just global, or also regional, friend, and guild?
Interviewer
Global is required. Friend leaderboard is a nice-to-have. Let's include regional too.
Candidate
Got it. Can scores decrease, or only increase? And is the leaderboard tracking personal best, current score, or a cumulative total?
Interviewer
Scores can only increase. The leaderboard tracks personal best — highest score ever achieved.
Candidate
Good — that simplifies writes a lot since most updates will be no-ops. Last two: do we need daily/weekly/all-time windows? And when a player finishes a match, do they need to see their exact rank immediately, or is a movement signal acceptable — "you moved up ~200,000 spots"?
Interviewer
Yes to time windows — daily, weekly, all-time. On rank: players care about seeing movement. Exact rank is less critical.
Candidate
Perfect. So functional requirements: global, regional, and friend leaderboards; personal best scoring, scores never decrease; daily/weekly/all-time windows; approximate rank with movement signal after each match.
Non-functional requirements 3:30 – 6:00
Candidate
For scale: I'll assume 100M registered players, ~10M daily active. Score updates happen after each match — peak maybe 500k updates/sec. Reads are heavier — players check leaderboards frequently, probably 2–3x write volume.
On consistency: since scores only increase, a few seconds of staleness on rank is acceptable. Availability over consistency. P99 read latency target: under 100ms.
On consistency: since scores only increase, a few seconds of staleness on rank is acceptable. Availability over consistency. P99 read latency target: under 100ms.
Interviewer
What about the friend leaderboard — same staleness tolerance?
Candidate
Friend leaderboards need tighter consistency — players know each other personally, they'll notice stale data. I'd target a few seconds lag at most and prioritize accuracy over perfect freshness there.
Entities 6:00 – 9:00
Candidate
Core entities: Player — player_id, display_name, region_id. Score — player_id, personal_best, last_updated. FriendRelation — player_id, friend_id, symmetric. Leaderboard — scope, time_window, ordered list of player+score.
The score entity is the source of truth. The leaderboard is always a derived view — never written to directly, always computed from scores.
The score entity is the source of truth. The leaderboard is always a derived view — never written to directly, always computed from scores.
Interviewer
Good distinction. How does the friend relation affect leaderboard design?
Candidate
Friend leaderboard is a personalized view — each player has their own ranked list of friends. The social graph determines which scores to include, but the scores come from the global score store. I'll cover the fan-out strategy in the deep dive.
APIs 9:00 – 12:00
Candidate
Four key endpoints.
The POST is write path; GETs are read path. They'll have very different performance characteristics.
POST /scores — body: player_id, new_score. Updates personal best if higher, triggers downstream leaderboard updates, returns rank delta.GET /leaderboard/{scope}/{window} — scope: global/regional/friend, window: daily/weekly/alltime. Paginated top-k with ranks.GET /leaderboard/{scope}/{window}/me — requesting player's rank plus nearby players ±5.GET /leaderboard/friend/{window} — friend leaderboard for requesting player.The POST is write path; GETs are read path. They'll have very different performance characteristics.
High level design 12:00 – 23:00
Candidate
Write path: Score update hits the Score Service, which checks personal best against a Redis hash. If the new score beats it, write to PostgreSQL first — source of truth — then publish a ScoreUpdated event to Kafka. Three consumers handle this asynchronously: Global, Regional, and Friend leaderboard consumers. This decouples the write spike from Redis and makes each leaderboard type independently scalable.
Storage: Redis sorted sets are the core — ZADD for writes, ZRANGE for top-k, ZREVRANK for exact rank. One sorted set per scope + window combination. PostgreSQL stores personal bests as the durable source of truth.
Read path: The Leaderboard Service serves all reads. Top-k hits Redis directly, cached with a short TTL. "My rank" uses a score histogram for approximate rank. Friend leaderboard merges a per-user cached sorted set with live fetches for high-degree users — I'll detail this in the deep dive.
Time windows: Each window is a separate sorted set. Daily and weekly keys carry TTLs aligned to the boundary.
Storage: Redis sorted sets are the core — ZADD for writes, ZRANGE for top-k, ZREVRANK for exact rank. One sorted set per scope + window combination. PostgreSQL stores personal bests as the durable source of truth.
Read path: The Leaderboard Service serves all reads. Top-k hits Redis directly, cached with a short TTL. "My rank" uses a score histogram for approximate rank. Friend leaderboard merges a per-user cached sorted set with live fetches for high-degree users — I'll detail this in the deep dive.
Time windows: Each window is a separate sorted set. Daily and weekly keys carry TTLs aligned to the boundary.
Interviewer
How do daily and weekly resets work exactly?
Candidate
Each time window is a separate key. Daily TTL is ~25 hours, weekly ~8 days. When a score updates, the consumer writes to all three windows. The all-time window only updates if it's a new personal best; daily and weekly always update since they track best score within the window. At reset, the old key expires and a new one starts accumulating. Before expiry, a background job can snapshot the top-k to cold storage for historical records.
Deep dive 23:00 – 42:00
Interviewer
Let's go deeper on rank. How does a player see their rank after the match? They have 100M other players in the system.
Candidate
This is where the problem diverges from top-k. ZREVRANK gives exact rank in O(log N), but at 100M players on a single node, millions of concurrent rank queries plus constant writes makes this a hot key problem.
My solution is a score histogram with a prefix sum array. Since the game has bounded discrete scores — say 0 to 10,000 in multiples of 50 — I have 200 fixed buckets. Each bucket stores a count of players in that score range. I maintain a prefix sum over those counts, recomputed every few seconds by a background job.
Approximate rank becomes a single array index read — O(1) from application memory, no Redis query needed. Error is bounded by bucket size. For rank movement delta: snapshot prefix_sum before and after the score update, return the difference.
My solution is a score histogram with a prefix sum array. Since the game has bounded discrete scores — say 0 to 10,000 in multiples of 50 — I have 200 fixed buckets. Each bucket stores a count of players in that score range. I maintain a prefix sum over those counts, recomputed every few seconds by a background job.
Approximate rank becomes a single array index read — O(1) from application memory, no Redis query needed. Error is bounded by bucket size. For rank movement delta: snapshot prefix_sum before and after the score update, return the difference.
Interviewer
You mentioned sharding. How would you shard the sorted set?
Candidate
Range-based sharding by score, aligned with the histogram bucket boundaries. 4 shards covering 0–2500, 2501–5000, 5001–7500, 7501–10000. Since we know the score distribution from game mechanics, I'd use equal-frequency boundaries so each shard holds roughly the same number of players.
Global rank = sum of ZCARD for all shards above mine — O(1) from cached bucket counts — plus ZREVRANK within my own shard at O(log N/shards). The bucket counts I already maintain for approximate rank double as shard cardinalities. These two systems reinforce each other.
Global rank = sum of ZCARD for all shards above mine — O(1) from cached bucket counts — plus ZREVRANK within my own shard at O(log N/shards). The bucket counts I already maintain for approximate rank double as shard cardinalities. These two systems reinforce each other.
Interviewer
What about the friend leaderboard?
Candidate
The key insight is the fan-out decision is made from the writer's perspective based on their friend count, not the reader's.
Normal users below a threshold — say 500 friends — fan out on write. When their score updates, the Kafka consumer writes their new score into each friend's cached sorted set. Reads are O(1).
High-degree users above the threshold skip fan-out. Their score lives only in the global sorted set. When any user opens their friend leaderboard, normal friends' scores come from the per-user cache, high-degree friends' scores are batch-fetched via ZSCORE from the global set, and the merge happens in application memory.
Friend add/remove: for fan-out-on-write users, immediately update both users' cached sorted sets. The friend list itself is cached separately from the graph DB.
Normal users below a threshold — say 500 friends — fan out on write. When their score updates, the Kafka consumer writes their new score into each friend's cached sorted set. Reads are O(1).
High-degree users above the threshold skip fan-out. Their score lives only in the global sorted set. When any user opens their friend leaderboard, normal friends' scores come from the per-user cache, high-degree friends' scores are batch-fetched via ZSCORE from the global set, and the merge happens in application memory.
Friend add/remove: for fan-out-on-write users, immediately update both users' cached sorted sets. The friend list itself is cached separately from the graph DB.
Interviewer
How do you handle a Redis shard going down?
Candidate
With replicas, the leaderboard continues to be served — reads fall back to replicas, which are slightly stale but acceptable. Writes buffer in Kafka during failover, so no score updates are lost within the Kafka retention window — I'd set that to at least 24 hours.
Redis Sentinel handles primary re-election automatically. If the entire shard is lost, a background job bootstraps the new node from PostgreSQL — batch SELECT scores within the shard's score range, ZADD in pipelines. Then replay Kafka events to catch up writes that arrived during recovery. This takes 10–30 minutes depending on shard size.
During that window, the affected score range can't be served — that's a real service interruption. The mitigation is replicas in separate availability zones so the probability of all copies being lost simultaneously is very low.
Redis Sentinel handles primary re-election automatically. If the entire shard is lost, a background job bootstraps the new node from PostgreSQL — batch SELECT scores within the shard's score range, ZADD in pipelines. Then replay Kafka events to catch up writes that arrived during recovery. This takes 10–30 minutes depending on shard size.
During that window, the affected score range can't be served — that's a real service interruption. The mitigation is replicas in separate availability zones so the probability of all copies being lost simultaneously is very low.
Assessment
Requirements✓ Sharp questions, clean summary, under 3:30
Entities + APIs✓ Source of truth vs derived view distinction made early
High level design✓ Working system, didn't over-optimize upfront
Rank at scale✓ Histogram + prefix sum, correct complexity analysis
Sharding✓ Range-based, aligned with buckets, shard cardinalities insight
Friend leaderboard✓ Fan-out decision from writer's perspective, correct merge
Failure handling✓ Full failure spectrum covered, Kafka as recovery mechanism
2. Speed round#
Eight questions an interviewer would ask. Read the question, form your own answer, then compare. The goal is to answer in 2–3 sentences without rambling.
Question 1
What clarifying questions would you ask before designing a leaderboard system?
What leaderboard scopes are needed — global, regional, friend, guild? Is the social graph symmetric friends or asymmetric follows, and is there a cap? Is the leaderboard tracking personal best, current score, or cumulative total — and can scores decrease? Do players need exact rank or is a movement signal sufficient? Do we need time windows — daily, weekly, all-time?
Start with product shape before scale numbers. Each answer should visibly change something in your design.
Question 2
A player just set a new personal best. Walk me through what happens in the system.
Score Service receives the update, checks the current personal best from a Redis hash, and since the new score is higher, writes to PostgreSQL first (source of truth), then publishes a ScoreUpdated event to Kafka. Kafka consumers asynchronously update the global, regional, and friend leaderboard sorted sets in Redis. The Score Service returns a rank delta to the client immediately, computed optimistically from the prefix sum before and after.
PostgreSQL before Kafka — always write to source of truth first. Rank delta is optimistic, not waited for.
Question 3
How does a player see their rank among 100M players?
Approximate rank via a score histogram. The score space is divided into buckets, each storing a player count. A prefix sum over those counts gives approximate rank in O(1) from application memory — no Redis call needed. Error is bounded by bucket size, which is acceptable since the product requirement is a movement signal, not exact rank.
Don't jump to ZREVRANK. It works on a single node but is a hot key at 100M scale. Prefix sum solves the read scalability problem.
Question 4
How does the friend leaderboard work? Assume symmetric friends capped at 500.
Symmetric + capped means fan-out on write. When a player sets a new personal best, a Kafka consumer ZADDs their score into each of their friends' per-user sorted sets — at most 500 writes. Reads are O(1), just serve the pre-built sorted set. Friend add/unfriend immediately updates both users' cached sorted sets.
The cap is the key — it bounds write amplification. Without a cap, you need the celebrity hybrid pattern.
Question 5
Your Redis node holding the global leaderboard is getting hammered. What do you do?
First, add read replicas — route all ZRANGE and ZREVRANK calls to replicas, keeping the primary for writes only. If write throughput is the bottleneck, shard by score range using equal-frequency boundaries aligned with the histogram buckets. Kafka consumers pipeline writes to each shard, keeping throughput well above the 100–150k writes/sec single-node ceiling.
Replicas first (cheaper), sharding second. Name what problem you're solving — read vs write bottleneck.
Question 6
How do you find the 5 players just above and below a given player?
Query by score proximity, not rank offset. Estimate a score window δ from bucket density (players per score point), then ZRANGEBYSCORE on the correct shard within (score - δ) to (score + δ), over-fetch slightly, trim to the closest 10. This is O(log N + M) and naturally shardable since a score range almost always hits exactly one shard.
Don't use ZREVRANK + ZRANGE by rank offset — rank offset requires global rank knowledge which breaks under sharding. Score is always exact even when rank is approximate.
Question 7
I need exact real-time rank for every player, updated within 1 second of any score change. How do you respond?
We can do exact rank — it changes the design meaningfully. Exact rank requires synchronous ZREVRANK plus ZCARD across all shards on every score update, adding latency to the write path and cross-shard coordination overhead. Before committing to that, I want to understand what surface needs this — exact rank on a post-match summary screen is much cheaper than exact rank updated in real-time during gameplay. Can you tell me the specific use case?
Push back with tradeoffs and a clarifying question. Don't collapse immediately and don't refuse to engage.
Question 8
How do you handle a Redis shard going down?
Reads fall back to replicas — slightly stale but acceptable. Writes buffer in Kafka during failover so no updates are lost within the retention window. Redis Sentinel handles primary re-election automatically. If the entire shard is lost, bootstrap from PostgreSQL via batch SELECT and ZADD pipeline, then replay Kafka events to catch up. Replicas in separate availability zones make total shard loss very unlikely.
Cover the full failure spectrum: partial failure (primary down) and total failure (full shard lost). Kafka is both a write buffer and a recovery mechanism.