Problem RestatementProblem
Design a real-time leaderboard system for a massively multiplayer online game that displays the top K players (e.g., top 100) based on scores. Core challenges include handling millions of score updates per second, maintaining sorted rankings efficiently, supporting multiple leaderboard types (global, regional, daily, weekly), and serving read requests with low latency (< 100ms).
RequirementsRequirements
1.1 Functional
- Update scores: Players earn points, update their scores in real-time.
- Top K leaderboard: Display top 100 players globally.
- Player rank: Show a specific player's current rank.
- Neighboring ranks: Show players ranked around a specific player (e.g., rank 95-105 for player at rank 100).
- Multiple leaderboards: Support global, regional (by country), and time-based (daily, weekly, monthly).
- Historical leaderboards: View past leaderboards (e.g., last month's top 100).
- Rewards: Distribute rewards to top K players at end of period.
1.2 Non-Functional
- Low Latency:
- Read (get top K): < 100ms.
- Write (update score): < 50ms.
- Scalability: Handle 100M active players, 1M score updates/sec.
- Accuracy: Rankings must be correct (strong consistency for writes, eventual consistency for reads acceptable).
- Availability: 99.9% uptime.
- Real-Time: Leaderboard updates within 1-2 seconds of score change.
1.3 Scale Estimates
Active players
100 million
Concurrent players
10 million at peak
Leaderboard reads
10M reads/sec (players check rankings)
Total leaderboards
1 + 200 + (3 × 201) = 804 leaderboards
- Score updates: 1M updates/sec (players complete matches, earn points).
- Leaderboard types:
- 1 global.
- 200 regional (countries).
- 3 time-based per type (daily, weekly, monthly).
1.4 API Design
The core APIs required for the service:
/v1/scoreUpdate ScoreUpdate player score./v1/leaderboard/:id/topGet Top KGet top K players./v1/leaderboard/:id/rank/:player_idGet RankGet player rank./v1/leaderboard/:id/neighbors/:player_idGet NeighborsGet surrounding ranks.High-Level ArchitectureArchitecture
2.1 Overview
- Write Path: Score Update → Score Service → Update Leaderboard (Sorted Set) → Cache.
- Read Path: Client → API Gateway → Leaderboard Service → Cache/DB → Return Top K.
- Key components: Redis Sorted Sets for rankings, sharding for scale, caching for reads.
2.2 Architecture Diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart TB
%% Game Clients
Player["Player<br/>(Game Client)"] -->|"1. Score Update"| AG["API Gateway"]
Player -->|"6. Get Top 100"| AG
%% Score Update Flow
AG -->|"2. POST /score"| ScoreService["Score Service"]
ScoreService -->|"3. Validate & Dedupe"| Redis1["Redis<br/>(Idempotency)"]
ScoreService -->|"4. ZINCRBY delta"| LeaderboardDB["Leaderboard DB<br/>(Redis Sorted Set)"]
%% Update Multiple Leaderboards
LeaderboardDB -->|"5a. Global"| GlobalLB["Global Sorted Set"]
LeaderboardDB -->|"5b. Regional"| RegionalLB["Regional Sorted Set<br/>(country-based)"]
LeaderboardDB -->|"5c. Time-Based"| TimeLB["Time-Based Sorted Set<br/>(daily/weekly/monthly)"]
%% Read Flow (Top K)
AG -->|"7. GET /leaderboard/top100"| LBService["Leaderboard Service"]
LBService -->|"8. Check Cache"| ReadCache["Read Cache<br/>(Redis)"]
ReadCache -->|"9a. Cache Hit"| Player
ReadCache -->|"9b. Cache Miss"| Query["Query Sorted Set"]
Query -->|"10. ZREVRANGE 0 99"| LeaderboardDB
LeaderboardDB -->|"11. Top 100"| LBService
LBService -->|"12. Cache Result"| ReadCache
LBService -->|"13. Return Top 100"| Player
%% Player Rank Query
AG -->|"R1. GET /rank/{player_id}"| LBService
LBService -->|"R2. ZREVRANK"| LeaderboardDB
LeaderboardDB -->|"R3. Rank"| Player
%% Analytics
ScoreService -->|"A1. Log Event"| Analytics["Analytics Service"]
Analytics -->|"A2. Store Events"| AnalyticsDB[(Analytics DB)]
%% Archival
TimeLB -->|"E1. End of Period"| Archival["Archival Service"]
Archival -->|"E2. Snapshot"| ArchiveDB[(Archive DB)]
Archival -->|"E3. Rewards"| Rewards["Reward Service"]
%% Styling
classDef client fill:#e3f2fd,stroke:#1976d2,stroke-width:2px;
classDef service fill:#c8e6c9,stroke:#388e3c,stroke-width:2px;
classDef storage fill:#fff9c4,stroke:#f57f17,stroke-width:2px;
classDef lb fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
class Player client;
class AG,ScoreService,LBService,Analytics,Archival,Rewards service;
class Redis1,ReadCache,AnalyticsDB,ArchiveDB storage;
class LeaderboardDB,GlobalLB,RegionalLB,TimeLB lb;Components (what & why)
Game Client
- Send score updates when player earns points.
- Request leaderboard data (top K, player rank).
- Display rankings in game UI.
API Gateway
- Route requests to appropriate services.
- Rate limiting (prevent abuse).
- Authentication.
Score Service
- Responsibilities:
- Validate score updates (anti-cheat).
- Deduplicate updates (idempotency).
- Update all relevant leaderboards (global, regional, time-based).
- Anti-Cheat: Verify score changes are legitimate (not too large).
Leaderboard DB (Redis Sorted Sets)
- Structure: Sorted set with player_id as member, score as value.
- Operations:
ZINCRBY leaderboard delta player_id: Atomically add points to a player's score.ZADD leaderboard score player_id: Set an absolute score (e.g. when rebuilding from the DB).ZREVRANGE leaderboard 0 99: Get top 100 players (descending).ZREVRANK leaderboard player_id: Get player's rank.ZSCORE leaderboard player_id: Get player's score.- Why Redis: In-memory, O(log n) operations, atomic updates.
Read Cache (Redis)
- Cache top K leaderboard results.
- TTL: 1-5 seconds (balance freshness vs load).
- Cache Key:
leaderboard:global:top100.
Leaderboard Service
- Responsibilities:
- Fetch top K from cache or DB.
- Fetch player rank.
- Fetch neighboring ranks.
- Optimization: Batch queries, parallel fetches.
Analytics Service
- Log all score updates for analytics.
- Detect anomalies (cheating, bugs).
Archival Service
- Snapshot leaderboards at end of period (daily, weekly, monthly).
- Store in Archive DB for historical queries.
- Trigger reward distribution.
Reward Service
- Calculate rewards for top K players.
- Distribute in-game currency, items, badges.
Data ModelData model
Leaderboard (Redis Sorted Set)
leaderboard:global -> {
player_123: 95000,
player_456: 89000,
player_789: 87500,
...
}Player
Player(
player_id,
username,
country,
total_score,
created_at
)Score Event (Analytics)
ScoreEvent(
event_id,
player_id,
score_delta,
new_score,
timestamp,
match_id
)Archived Leaderboard
ArchivedLeaderboard(
leaderboard_id,
type, -- GLOBAL, REGIONAL, DAILY, WEEKLY, MONTHLY
region,
start_date,
end_date,
top_k_snapshot -- JSON: [{player_id, score, rank}, ...]
)Key FlowsFlows
5.1 Update Score Flow
- Player completes match, earns 500 points.
- Game client calls
POST /score {player_id: 123, score_delta: 500, match_id}. - Score Service validates update (anti-cheat check).
- Score Service checks idempotency (skip if already processed).
- Score Service updates leaderboards:
- Global:
ZINCRBY leaderboard:global 500 player_123. - Regional:
ZINCRBY leaderboard:us 500 player_123. - Daily:
ZINCRBY leaderboard:daily:2023-10-20 500 player_123. - All three in one
MULTI/pipeline.ZINCRBYadds the delta atomically on the server; a read-then-ZADDwould lose points when two matches finish at the same time.
- Analytics Service logs event.
5.2 Get Top K Flow
- Player opens leaderboard screen, requests top 100.
- Client calls
GET /leaderboard/global/top100. - Leaderboard Service checks Read Cache.
- Cache hit: Return cached result.
- Cache miss:
- Query:
ZREVRANGE leaderboard:global 0 99 WITHSCORES. - Redis returns top 100 players with scores.
- Fetch player usernames from Player DB (batch query).
- Cache result with 2-second TTL.
5.3 Get Player Rank Flow
- Player wants to know their current rank.
- Client calls
GET /rank/{player_id}. - Leaderboard Service queries:
ZREVRANK leaderboard:global player_123. - Redis returns rank (e.g., 1245).
- Return to client: "You are ranked #1245".
5.4 Get Neighboring Ranks Flow
- Player at rank 1245 wants to see ranks 1240-1250.
- Client calls
GET /leaderboard/global/neighbors/{player_id}. - Leaderboard Service:
- Get player rank:
ZREVRANK leaderboard:global player_123→ 1245. - Fetch range:
ZREVRANGE leaderboard:global 1240 1250 WITHSCORES.
5.5 End-of-Period Flow (Daily Leaderboard)
- Cron job triggers at midnight (end of day).
- Archival Service:
- Fetches entire daily leaderboard:
ZREVRANGE leaderboard:daily:2023-10-20 0 -1 WITHSCORES. - Saves snapshot to Archive DB.
- Distributes rewards to top 100 players.
DEL leaderboard:daily:2023-10-20.
- Creates new leaderboard for next day.
Deep Dive A: Efficient Data Structures for Top K (~10 mins)Deep dive
Problem
Store and rank 100M players efficiently. Need O(log n) updates and O(K) Top K queries.
Solution: Redis Sorted Set
Why Redis Sorted Set?
- In-Memory: Ultra-fast reads/writes.
- Sorted: Automatically maintains sorted order by score.
- Atomic Operations: ZINCRBY, ZADD, ZREVRANGE are atomic.
- Fits in memory: 100M members × ~100 bytes (member + score + skiplist/hash overhead) ≈ 10 GB — one node can hold it. What one node can't do is 1M updates/sec across several leaderboards (see Deep Dive B).
- Logarithmic Complexity: O(log n) for insert/update.
Key Operations
# Add points to a player's score (atomic read-modify-write)
ZINCRBY leaderboard:global 500 player_123
# Set an absolute score
ZADD leaderboard:global 95000 player_123
# Get top 100 (descending by score)
ZREVRANGE leaderboard:global 0 99 WITHSCORES
# Get player's rank (0-indexed)
ZREVRANK leaderboard:global player_123
# Get player's score
ZSCORE leaderboard:global player_123
# Get count of players
ZCARD leaderboard:globalTime Complexity
- Insert/Update: O(log n).
- Get Top K: O(log n + K).
- Get Rank: O(log n).
- Get Score: O(1).
Tie-Breaking
Two players with 95,000 points get an arbitrary order (Redis falls back to comparing member names). The usual fix is to fold the tie-breaker into the score: whoever reached the score first ranks higher.
score = points × 10^10 + (MAX_TS − timestamp_of_last_score_change)A sorted-set score is a 64-bit float with 53 bits of exact integer precision, so budget the bits: e.g. points up to ~900K with a seconds-precision timestamp offset. Display floor(score / 10^10) as the points.
Alternative: Min Heap (In-Memory)
Approach
- Maintain min heap of size K (top K players).
- When new score comes:
- If score > heap min: Replace min with new score, heapify.
- Else: Ignore.
Pros
- Space Efficient: Only store K items (vs all players).
- Fast Reads: O(1) to get top K.
Cons
- Slow Writes: O(n) to find player for update.
- No Full Ranking: Can't get rank for players outside top K.
When to Use
- Only need top K (no full ranking).
- K is small (e.g., 100).
Hybrid Approach (Sharding + Sorted Set)
Problem
Memory is not the constraint (~10 GB for 100M players). Throughput is: each score update touches ~3 leaderboards (global, regional, daily), so 1M updates/sec is ~3M sorted-set writes/sec — more than one Redis node (~100K–200K ops/sec for these commands) can absorb.
Solution
- Shard by Score Range:
- Shard 1: Scores 0-1M.
- Shard 2: Scores 1M-2M.
- Shard 3: Scores 2M+.
- Top K Query: Query top K from each shard, merge results.
Data Structure Comparison
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart TD
Problem["Top K Problem<br/>(100M players)"]
Problem --> RS["Redis Sorted Set"]
Problem --> Heap["Min Heap"]
Problem --> Shard["Sharded Sorted Set"]
RS --> RSPros["✅ O(log n) ops<br/>✅ Full ranking<br/>✅ Atomic"]
RS --> RSCons["❌ Single-node write throughput<br/>❌ All data in RAM"]
Heap --> HeapPros["✅ Space efficient<br/>✅ O(1) top K read"]
Heap --> HeapCons["❌ No full ranking<br/>❌ Slow updates"]
Shard --> ShardPros["✅ Horizontal scale<br/>✅ Full ranking"]
Shard --> ShardCons["❌ Complex merging<br/>❌ Cross-shard queries"]
classDef pros fill:#c8e6c9,stroke:#388e3c,stroke-width:2px;
classDef cons fill:#ffcdd2,stroke:#c62828,stroke-width:2px;
class RSPros,HeapPros,ShardPros pros;
class RSCons,HeapCons,ShardCons cons;Deep Dive B: Sharding Strategies (~10 mins)Deep dive
Problem
100M players fit in one Redis node's memory (~10 GB), but ~3M sorted-set writes/sec (1M updates × 3 leaderboards) and 10M reads/sec do not fit in one node's throughput. We need to spread writes — and still answer "top K" and "what is my rank?" globally.
Sharding Strategies
Strategy 1: Shard by Player ID (Hash-Based)
player_123 -> Shard 3 (hash(player_123) % 10 = 3)
player_456 -> Shard 6 (hash(player_456) % 10 = 6)Top K Query
- Query top K from each shard.
- Merge results globally.
- Example: Top 100 global.
- Fetch top 100 from each of 10 shards.
- Merge 1000 results, select top 100.
Pros:
- Even distribution of players.
- Parallel queries.
Global Rank
ZREVRANK on a player's own shard is only their rank *within that shard*. Global rank = number of players with a higher score on every shard:
rank(player) = Σ over shards ZCOUNT(leaderboard, "(" + score, "+inf")That is one O(log n) call per shard, sent in parallel — fine for 10–20 shards. At larger scale, keep a per-shard histogram of score buckets and return an approximate rank ("top 3%") for players outside the top ~10K, which is what most games show anyway.
Cons:
- Must query all shards for top K and for global rank.
- Merge overhead.
Strategy 2: Shard by Score Range
Score 0-10K -> Shard 0
Score 10K-100K -> Shard 1
Score 100K+ -> Shard 2Top K Query
- Start with highest score shard (Shard 2).
- Fetch top K from Shard 2.
- Optimization: If Shard 2 has ≥ K players, done.
Pros:
- Top K query very fast (only query top shards).
- Natural sharding by performance tier.
Global Rank
- Easy: rank = (players in all higher-score shards, via
ZCARD) +ZREVRANKin the player's own shard.
Cons:
- Uneven distribution (top shard may be hot).
- Need to rebalance as score distribution changes.
- A player whose score crosses a boundary must move shards (remove from one, add to another) — not atomic across nodes, so briefly visible in both or neither.
Strategy 3: Geo-Sharding (Regional Leaderboards)
US players -> US Shard
EU players -> EU Shard
Asia players -> Asia ShardUse Case:
- Regional leaderboards (no global merge needed).
Pros:
- Perfect for regional leaderboards.
- Low latency (geo-distributed).
Cons:
- Global leaderboard requires merging all regions.
Recommended: Hybrid
- Regional Leaderboards: One sorted set per region; most fit on a single node.
- Global Leaderboard: Shard by player ID for even write load. Top K = merge each shard's top K. Global rank = parallel
ZCOUNTacross shards (exact), or score-bucket histograms (approximate) outside the top 10K. - Optimization: Cache global top K (refresh every 2 seconds) — it is the most-read and most expensive query.
Sharding Architecture
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart TB
TopK["Top K Query"] --> Coord["Query Coordinator"]
Coord -->|"parallel query"| S1["Shard 1<br/>(players 1-10M)"]
Coord -->|"parallel query"| S2["Shard 2<br/>(players 10M-20M)"]
Coord -->|"parallel query"| S10["Shard 10<br/>(players 90M-100M)"]
S1 -->|"top 100"| Merge["Merge Results"]
S2 -->|"top 100"| Merge
S10 -->|"top 100"| Merge
Merge -->|"global top 100"| Cache["Cache Result<br/>(2s TTL)"]
Cache --> TopK
classDef shard fill:#fff9c4,stroke:#f57f17,stroke-width:2px;
classDef merge fill:#c8e6c9,stroke:#388e3c,stroke-width:2px;
class S1,S2,S10 shard;
class Coord,Merge,Cache merge;Deep Dive C: Real-Time Updates & Caching (~8 mins)Deep dive
Problem
1M score updates/sec, 10M leaderboard reads/sec. Need low latency without overwhelming DB.
Write Optimization
Batching
- Accumulate score updates for 1-2 seconds.
- Batch update Redis in single operation.
- Trade-off: Slight delay (1-2s) acceptable for leaderboard.
Idempotency
- Use idempotency key (match_id + player_id).
- Store in Redis with TTL (5 minutes).
- Check: Before applying update, check if already processed.
Read Optimization
Multi-Layer Caching
L1: Application Cache (In-Memory)
- Cache top K in application memory (each API server).
- TTL: 2 seconds.
- Benefit: Serve reads without Redis query.
L2: Redis Cache
- Cache top K results in Redis.
- TTL: 5 seconds.
- Key:
cache:leaderboard:global:top100.
L3: Redis Sorted Set (Source of Truth)
- Authoritative leaderboard data.
Cache Invalidation Strategy
Time-Based (TTL)
- Cache expires after TTL (e.g., 2 seconds).
- Next read refreshes cache.
- Pros: Simple, works well for high-read scenarios.
Write-Through
- On score update, invalidate cache immediately.
- Cons: High write rate → constant cache invalidation.
Hybrid (Recommended)
- Use TTL (2-5 seconds).
- Accept slightly stale data (eventual consistency).
Read/Write Flow Optimization
# Write (Score Update)
def update_score(player_id, match_id, score_delta):
# Idempotency: the same match must not be counted twice (SET NX = only first caller wins)
if not redis.set(f"scored:{match_id}:{player_id}", 1, nx=True, ex=300):
return
# ZINCRBY adds the delta atomically, so concurrent updates never lose points
pipe = redis.pipeline(transaction=True)
pipe.zincrby("leaderboard:global", score_delta, player_id)
pipe.zincrby(f"leaderboard:{region}", score_delta, player_id)
pipe.zincrby(f"leaderboard:daily:{date}", score_delta, player_id)
pipe.execute()
# No cache invalidation (TTL handles it)
# Read (Top K)
def get_top_k(k=100):
# Check L1 cache (app memory)
if app_cache.has("top100"):
return app_cache.get("top100")
# Check L2 cache (Redis)
if redis.exists("cache:top100"):
result = redis.get("cache:top100")
app_cache.set("top100", result, ttl=2)
return result
# Query source (Redis Sorted Set)
result = redis.zrevrange("leaderboard:global", 0, 99, withscores=True)
# Cache result
redis.setex("cache:top100", 5, result)
app_cache.set("top100", result, ttl=2)
return resultCaching Architecture
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
Read["Read Request"] --> L1["L1: App Cache<br/>(2s TTL)"]
L1 -->|"miss"| L2["L2: Redis Cache<br/>(5s TTL)"]
L2 -->|"miss"| L3["L3: Redis Sorted Set<br/>(Source of Truth)"]
L1 -->|"hit"| Return["Return Result<br/>(<0.1ms, no network)"]
L2 -->|"hit"| Return2["Return Result<br/>(~1ms)"]
L3 -->|"query"| Return3["Return Result<br/>(~1-2ms per shard + merge)"]
classDef cache fill:#c8e6c9,stroke:#388e3c,stroke-width:2px;
class L1,L2,L3 cache;Scaling & Performance (~5 mins)Scale
Horizontal Scaling
- Score Service: Stateless, scale with load balancer.
- Leaderboard Service: Stateless, scale horizontally.
- Redis: Shard across multiple instances.
Performance Metrics
- Write latency: ~1–5ms end to end (a Redis
ZINCRBYitself is well under 1ms; the rest is network and the idempotency check). - Read latency: <1ms from app cache, ~1–2ms from Redis, a few ms for a cache-miss top-K merge across shards.
- Throughput: 1M writes/sec, 10M reads/sec.
Bottlenecks & Mitigations
- Redis Write Throughput: Shard by player ID; batch updates per shard with pipelining.
- Read Load: Multi-layer caching (app cache + Redis cache).
- Write Load: Batch updates, async processing.
Failure Modes & Recovery
Redis Failure
- Impact: Leaderboard unavailable.
- Mitigation: Redis Cluster with replication (3× replicas).
Cache Invalidation Storm
- Impact: Cache expires for popular leaderboard → all requests hit DB.
- Mitigation: Probabilistic early expiration (jitter in TTL).
Score Update Loss
- Impact: Player score not updated.
- Mitigation: Log all updates to Kafka, replay on failure.
Trade-offs & AlternativesTrade-offs
Redis vs Database
- Redis: In-memory, fast, limited durability.
- Database: Persistent, slower, complex indexing.
- Choice: Redis for hot data, DB for archival.
Strong vs Eventual Consistency
- Strong: All players see same leaderboard immediately.
- Eventual: Slight delays acceptable (1-2 seconds).
- Choice: Eventual (caching with TTL).
Real-Time vs Batch Processing
- Real-Time: Update leaderboard immediately.
- Batch: Update every 5 minutes.
- Choice: Real-time with 1-2 second caching.
Security & Anti-Cheat
Score Validation
- Verify score changes are within expected range.
- Flag suspicious updates (e.g., 100K points in 1 second).
Rate Limiting
- Limit score updates per player (e.g., max 10/minute).
Audit Logs
- Log all score updates for forensic analysis.
Interview Time Allocation (45 min)
- 5 min: Requirements & scope (functional, non-functional, scale).
- 10 min: HLD & architecture diagram (write + read paths).
- 5 min: Data model & key flows (update, top K, rank).
- 10 min: Deep dive on efficient data structures (Redis Sorted Set vs Min Heap).
- 10 min: Deep dive on sharding strategies (player ID, score range, geo).
- 5 min: Real-time updates, caching, scaling, failure handling.
SummaryWrap-up
- Core Challenges: Efficiently rank 100M players, handle 1M writes/sec and 10M reads/sec, real-time updates with low latency.
- Key Components:
- Redis Sorted Set: O(log n) updates, O(K) Top K queries.
- Sharding: Partition by player ID, merge results for global Top K.
- Multi-Layer Caching: App cache (2s) + Redis cache (5s) + Sorted Set.
- Idempotency: Prevent duplicate updates.
- Scaling Strategy: Shard Redis, cache aggressively, batch updates.
- Performance: ~1–5ms writes, ~1ms cached reads, 1M writes/sec, 10M reads/sec.
This design supports real-time leaderboards for massive multiplayer games with millions of players and billions of score updates daily.