System Architecture

Design a Real-Time Gaming Leaderboard

Design a real-time leaderboard with idempotent scoring, fast exact-rank reads, and safe recovery.

Advanced·

Design a tournament leaderboard that accepts verified match results, returns the top ten and a player’s neighbors quickly, and survives retries and cache loss.

1. Requirements and scope

Problem statement

A leaderboard is a real-time Top K ranking system for players. It continuously orders players by their changing scores and shows the leading part of that ordered list—such as the Top 10 players in a tournament—along with an individual player’s own place and nearby rivals. After a match, the player’s score changes on the relevant tournament, season, or game-mode board.

The same Top K ranking pattern appears outside gaming: most-liked videos, trending songs, popular posts, highest-rated products, and most-played games are all ordered lists whose leading entries change as new activity arrives. This article uses the gaming form of that pattern as its running example, covering daily contests, monthly seasons and rolling “last seven days” boards.

Functional requirements

  • Apply verified points once within the documented dedupe horizon. When the game service confirms a match result, add the award (one points change for one player) to the correct player’s score. If that result is sent again within that horizon because of a retry, do not add the points a second time.
  • Show the leaderboard. For each board, return the Top 10 players (or another capped requested count), including each player’s score and position.
  • Show a player’s context. Return a player’s exact position and score, plus up to four players immediately above and four immediately below them. The rank is exact for the board’s home region; a worldwide rank may catch up after cross-region updates arrive.
  • Handle tied scores consistently. When players have the same score, always use the same published tie-break rule so their displayed order does not change randomly.
  • Expose projection freshness. Return asOf, the projection position included in a read, so a caller can tell what the response contains —for example, “As of 10:20 PM” or “2 minutes ago.”
  • Keep active viewers informed. Send a small live update when a subscribed player’s rank, score, or the Top 10 changes. Clients can fetch the board again if they miss an update; they should not repeatedly download the whole board.
  • Finish and preserve a board. At the end of a tournament or season, stop normal scoring, save an immutable historical result, and support a documented correction process for late or reversed decisions.

Scope and exclusions

The system starts after match validation and ends at ranking delivery. Match adjudication, anti-cheat, matchmaking, prize settlement, arbitrary analytical filters, and exact worldwide ordering during a regional partition are outside this design.

Non-functional requirements and targets

These are load-test targets, not measured deployment results.

PropertyTarget
Availability99.9% monthly for score acceptance and reads; projection failure may temporarily serve stale ranks.
Read latencyWarm top-ten and player-context reads: p95 < 50 ms and p99 < 150 ms in the home region.
Write latencyDurable score acceptance: p99 < 200 ms.
FreshnessKafka acceptance to visibility in Redis (the in-memory ranking database): p95 < 2 s and p99 < 5 s.
ConsistencyOne durable total per player and period; Redis is an eventually consistent projection (a rebuildable copy optimized for reads). Responses include asOf (the update position included in the response).
DurabilityAcknowledge only after Kafka replicates the award according to acks=all, the configured replication factor, and min.insync.replicas. A broker failure must not lose an acknowledged award.
Scale5M DAU, 25M MAU, each player can submit up to 10 accepted awards per board every day, 5x peak, and 25M members in a large monthly board.

2. Capacity estimate

Count one award per player and board. A match that contributes to several boards produces one award per board, so use B for the average board fan-out. The baseline below uses B = 1, ten leaderboard reads per daily user per day, 100 stored bytes per event before indexes and replication, and a 200-byte message.

FigureArithmeticEstimate
Award writes5M x 10 x B / 86,400579 x B/s average; 2,894 x B/s at 5x peak
Leaderboard reads5M x 10 / 86,400579/s average; 2,894/s at 5x peak
Event storage5M x 10 x B x 100 B x 30150 x B GB/month raw
Score ingress2,894 x B/s x 200 B0.58 x B MB/s before protocol, replication, and fan-out
ZSET payload floor25M x 26 B nominal member-plus-score payload650 MB raw; a ZSET is Redis’s sorted-set data structure, so actual Redis memory is materially higher

The ZSET and version-map figures are payload floors; measure representative keys with allocator overhead, replicas, persistence buffers, and the production command mix. At this scale, one ZSET easily fits a suitably sized Redis primary; sharding (splitting data across several machines) is a future-constraint design, not the starting point.

3. Interfaces and data model

Durable model and read projection

Durable score model Durable score modelDurable score model
Awards remain replayable while the read projection stays optimized for rank queries.
  • A SCORE_EVENT represents one player’s award on one leaderboard.
  • Its unique identity is (leaderboardId, matchId, userId, awardType), preventing duplicate awards.
  • One match may award multiple players across multiple leaderboards.
  • Each leaderboard record stores a player’s current total and version.
  • Player profiles remain separate from leaderboard data.

API surface

APIContract
POST /internal/awardsInternal validated-game-service endpoint. Send matchId, leaderboardId, userId, awardType, and delta; the canonical event ID keys the request. Return 202 Accepted only after replicated Kafka acceptance—not after first-time application or projection.
GET /v1/leaderboards/{id}/top?k=10Ordered {userId, rank, score} records plus asOf; cap k.
GET /v1/leaderboards/{id}/users/{userId}/context?range=4Target player and up to four neighbors on both sides; return fewer at a boundary.
GET /v1/leaderboards/{id}/users/{userId}Score and exact rank.
WS /ws/leaderboards/{id}A WebSocket (a persistent server-client connection) that sends versioned score_updated, rank_changed, and top_k_changed hints for one board.

4. High-level architecture

Architecture at a glance

A validated game result produces an idempotent award event. Kafka durably accepts the event, and a stateful processor calculates the player’s new total. The total is projected to Redis for fast rank reads, while DynamoDB stores an asynchronous audit copy. The read service gets rankings from Redis and player details from the profile store.

For a smaller deployment, Kafka and DynamoDB can be replaced with a relational award ledger and a worker. Section 6 explains the storage choices and trade-offs.

Kafka-first architecture Kafka-first architectureKafka-first architecture
Verified awards flow through a durable stream before reaching the fast ranking projection.

Component responsibilities: Kafka-first design

The diagram shows the main request and data paths; Sections 5 and 7 detail the ranking pipeline and failure handling.

ComponentWhat it doesWhat it does not own
Game clientRequests Top K, its own rank/context, and subscribes to live hints.It never submits a score or decides a match outcome.
Validated scoring pathAdjudicates the match, assigns the canonical event ID, and publishes the trusted award.Leaderboard reads and rank calculation.
Kafka awards topicReplicated, partitioned durable intake log for score awards.Ranking queries or a mutable database aggregate.
Ranking pipelineDeduplicates awards, calculates versioned totals, and updates downstream projections. Its changelog, absolute-score topic, and projection writers are detailed later.Client-facing reads.
Redis ranking projectionHolds the sorted-set score index and version map used for Top K, exact rank, and neighbor reads.Durable acceptance; it is rebuilt from restored processor state and the recorded Kafka offset.
DynamoDB audit historyStores immutable award history for audit, reconciliation, and export.Exact rank or the live acceptance path.
Leaderboard APIReads a bounded rank/window from Redis, adds profile data, and returns asOf.Computing scores or changing rankings.
Profile storeResolves player IDs into display data outside the ranking structure.Rank order and score totals.
Notification deliverySends bounded change hints and requires clients to refetch after gaps.A durable event log or full-board state.
  • This is the primary design at every stated load. The game client never submits points; only the validated scoring path publishes awards.
  • Kafka is the acceptance boundary. The scoring path acknowledges only after an idempotent producer receives replicated acks=all acknowledgement with the configured minimum in-sync replicas. An audit-store outage cannot lose an accepted award.
  • Partition by (leaderboardId, userId) to preserve a player’s event order. The ranking pipeline emits versioned absolute totals so its sinks can retry safely.
  • At higher write rates, add Kafka partitions and matching processor capacity; monitor consumer lag, state-restore time, and the command latency of any hot Redis board.
  • The Leaderboard API gets ordered IDs and scores from Redis, then batches profile decoration.
  • The idempotent producer de-duplicates only its own broker retries. The processor drops repeated event IDs within the documented dedupe horizon; the acknowledgement means durable acceptance, not first-time application.

5. Core flows

Write path and retry semantics

Award acceptance and projection Award acceptance and projectionAward acceptance and projection
Durable acceptance is separate from idempotent projection into Redis and audit history.
  • The acknowledgement means replicated Kafka acceptance. Each projection partition publishes its processed source offset. The read service returns those offsets as asOf; it must not claim a single time watermark across partitions unless the intake path assigns and preserves a board-wide sequence.
  • The canonical event ID is derived from (leaderboardId, matchId, userId, awardType). Include awardType in every request. The write service accepts an event durably; it cannot synchronously decide whether an existing event ID has a conflicting payload because it does not own dedupe state. The processor retains an event-ID payload digest within its dedupe horizon, drops an identical repeat, and sends a mismatch to reconciliation. Kafka partitioning serializes the aggregate for one player.
  • Kafka can redeliver after a crash. Versioned absolute totals make replay and reordering safe; DynamoDB uses a conditional event-ID write, and Redis applies only a newer version. Downstream consumers use isolation.level=read_committed, so they never apply aborted transactional output.
  • A Redis function or Lua server-side script updates the colocated version record and ZSET atomically. Plain ZINCRBY is unsafe for replay.
Projection strategyCorrectnessCost
ZINCRBY onlyDuplicate delivery double-awards.Lowest latency.
Event-ID dedupe in RedisSafe only while dedupe keys survive.Extra memory and expiry policy.
Durable total plus versioned ZADDDuplicate and out-of-order events cannot corrupt the score.Kafka state plus a small atomic Redis program.

Read path

Bounded rank read Bounded rank readBounded rank read
One bounded ranking query is decorated with player details before returning to the client.
  • Use the Redis primary for exact rank. Fetch rank and its neighbor window in one Redis function so a concurrent update cannot move the player out of its own response window. A replica is acceptable for top ten only when the product tolerates lag.
  • Profile lookup can degrade to IDs plus scores.
  • A second cache is unnecessary at this scale. If one board’s top ten is polled by a very large audience, a one-second in-process micro-cache of that result removes read amplification without invalidation complexity; return its projection asOf with the response.

6. Storage and indexing decisions

Decision summary

ConcernChoiceReason
Durable acceptance and replayKafka awards topicReplicated intake, replay, and independent consumers.
Hot ranking readsRedis ranking projectionEfficient Top K, exact rank, and bounded neighbor queries.
Audit and historyAsynchronous DynamoDB sinkImmutable audit storage without blocking score acceptance.
Smaller deploymentRelational ledger plus Redis workerSimpler when Kafka-scale replay and fan-out are not required.

Why Kafka-first instead of an RDBMS-only design

An RDBMS (a relational database such as PostgreSQL or MySQL) can accept this document’s 579–2,894 awards/s with a unique award constraint. Its weakness is not ingestion; it is serving hot Top K, exact-rank, and neighbor-window queries while maintaining an ordered index for a large active board. An arbitrary player’s exact rank is not a point lookup: it depends on all players ahead of that player.

The minimal baseline uses that relational ledger plus Redis. Kafka-first is chosen here for replay, independent audit/analytics/notification sinks, and headroom to 100K/s; Redis serves rank in either case.

DynamoDB for durable score history and audit

DynamoDB (a managed NoSQL key-value database) is the durable audit/history copy in the Kafka-first design. A sink consumes the versioned update record—which includes eventId, delta, total, and version—and writes each immutable event with a conditional put. Use a bucketed partition key to spread a busy board’s writes instead of concentrating them on one key.

Store the event ID, match ID, player ID, award type, delta, accepted time, aggregate version, and the processor’s source position. This supports audit, reconciliation, historical exports, and rebuilding without placing DynamoDB on the latency-critical acceptance path. The Kafka event and changelog remain the recoverable source for the live processor; a delayed DynamoDB write must not block a verified score from reaching the live board.

DynamoDB is also not the exact global-rank store. It orders items only inside one partition, and a write-sharded board would require reading and merging several partitions to produce Top K or an exact rank. Use it for durable event history; use Redis sorted sets for the hot ordered index. A DynamoDB Global Table is not a score-merging mechanism: concurrent mutable aggregates can be resolved as last writer wins, so replicate immutable events rather than overwriting a player total.

Why Redis is the ranking projection

Redis sorted sets are the hot, ordered read model: ZADD updates a score in O(log N), ZREVRANK finds a player’s ordinal rank in O(log N), and ZRANGE ... REV returns Top K or a small neighbor window in O(log N + K). This avoids repeatedly ranking the full dataset in the write database.

Redis is not the source of truth. A stateful score processor creates a versioned absolute total for each player. The projection writer atomically accepts the update only if its version is newer, then uses ZADD to set—not increment—the score. This prevents duplicate and out-of-order deliveries from awarding points twice. If Redis is lost, rebuild it from Kafka and the processor’s durable state; keep DynamoDB (or equivalent) for audit/history rather than live rank queries.

Use the canonical user ID as the Redis member and explicitly state the tie policy. Equal scores have deterministic member-ID ordering, which gives ordinal ranks. If the product needs “first to reach the score wins,” persist a first-reach sequence and include it in the ordering model; storing a timestamp alone does not affect ZSET order. Integer scores remain exact up to 2^53, so confirm that the score range fits.

In Redis Cluster, keep a board’s score set and version key in the same hash slot, for example lb:{boardId}:scores and lb:{boardId}:versions. This lets one atomic script update both safely. But one leaderboard still runs on one Redis primary: Cluster can spread many boards, not scale one very hot global board. For that case, partition the board or give up exact global ranks.

Simpler approachLimitation at these requirementsDesign response
Rank directly from an RDBMS tableCorrect for a small board, but hot ordered writes and arbitrary rank/window reads contend with the write database.Keep it for reporting if needed; use Kafka plus Redis for the live board.
Use Redis alone with relative ZINCRBY score incrementsRedis loss can erase accepted points; retrying an increment awards points twice.Store each award and absolute total durably; project only newer versions with ZADD, which sets an absolute score.
Write DynamoDB, then Redis directlyA crash between writes leaves the stores inconsistent and couples acceptance to two systems.Accept once in Kafka; use idempotent sinks for Redis and DynamoDB.
Poll the full board for live changesRepeated reads grow with subscribers.Push bounded WebSocket or SSE hints; clients refetch after a gap.

7. Reliability, scale, and lifecycle

Persistence, failover, and board lifecycle

Board lifecycle Board lifecycleBoard lifecycle
A board closes at a durable cutoff before its results become an immutable archive.
  • Keep an active board’s state durable and do not rely on a short Redis TTL. A player can be initialized at zero in the processor state; add them to the visible ZSET only if the product wants zero-score players ranked.
  • Closing is a barrier, not an instant archive. First, the game service stops accepting new awards. It then records the final accepted Kafka offset for every awards partition. Processors drain through those offsets; only when every partition has reached its cutoff can the board be frozen and exported. An event arriving after the cutoff is rejected or sent to the correction workflow, so a late delivery cannot silently change the archived result.
  • Redis replication is asynchronous, so failover can lose recent projected writes. After promotion, rewind the Redis projection consumer to a recorded safe offset and replay idempotently. For total Redis loss, restore processor state from its compacted Kafka changelog, rebuild a replacement key, catch up, then atomically swap it in. Kafka is the recovery source; Redis persistence only shortens recovery.
  • Store the final result as an immutable archive and serve historical reads from long-TTL caches or a CDN. Late adjudication must be an explicit product choice: reject it, amend the result and invalidate caches, or publish a corrected result version.

Scaling exact global rank

A hash shard assigns users to keys by user ID; a score band assigns them by score range. Global rank means position across every shard.

Scaling exact global rank Scaling exact global rankScaling exact global rank
Keep one ordered index while it meets targets; sharding introduces explicit rank trade-offs.
LayoutTop KExact player rankMain risk
One ZSETOne bounded rangeOne rank lookupOne primary’s RAM and command throughput
Hash shardsRead K per shard, then mergeSum ZCOUNT(score, +inf) across shards, then apply the deterministic tie ruleP parallel calls and tail latency
Score bandsRead highest nonempty bandsHigher-band counts plus local rankHot bands and recoverable cross-band moves
  • Start with one ZSET and benchmark representative reads, writes, memory, replicas, failover, and restore.
  • K entries from each of N hash shards are sufficient for global Top K: an entry below its shard’s Kth cannot enter the global K.
  • Hashing by userId mod N spreads writes. Exact rank is the sum of ZCOUNT(score, +inf) over every shard plus deterministic tie handling: O(log N) per shard, but with P parallel calls and their tail latency.
  • Score bands work only when ranges remain balanced. A score change may cross bands, and Top K may spill into lower bands.

Fixed and rolling time windows

Rolling-window projection Rolling-window projectionRolling-window projection
Large rolling boards maintain incremental deltas instead of rebuilding a full union at read time.
WindowWrite and read planTrade-off
Fixed monthWrite lb:{id}:202610, stop at close, and read that key.Exact boundary and simplest operation.
Last seven calendar daysWrite daily ZSETs; maintain a rolling result with add and expire deltas, then atomically swap its key.Fast reads; worker state and recovery complexity.
Exact last 168 hoursUse finer buckets or event-time expiry and subtraction.More storage and maintenance.
  • ZUNIONSTORE is a blocking Redis command: a union over a 25M-member board can block the instance for seconds. Use it only for small boards or on an isolated instance. For a large rolling board, a worker applies add and expiry deltas incrementally, builds a replacement key in the same Redis hash slot, and swaps it atomically. Calendar-day buckets are simpler but are not an exact moving 168-hour window.

Bottlenecks and failure handling

Failure or pressureResponse
Kafka lag exceeds the targetAlert on oldest unprojected age, scale workers, coalesce safe updates, and shed live push before durable writes.
Kafka producer cannot meet the acknowledgement policyReject or retry the award; never acknowledge it before the configured replicas confirm it.
DynamoDB audit sink lags or is unavailableKeep score acceptance and rank projection available; retry the idempotent sink and alert on its lag.
One Kafka partition is hotUse enough partitions and the (leaderboardId, userId) key; a single extremely active player still serializes by design.
One tournament saturates a Redis primaryMove to an explicit sharded rank design; a Cluster slot move does not increase per-key capacity.
Profile database slowsBatch and cache profiles, use a circuit breaker, and return IDs plus scores when decoration times out.
Subscriber fan-out surgesBound queues, coalesce hints, drop slow consumers, and require REST resync.
Kafka broker failsContinue only while the producer can satisfy the acknowledgement policy; replace the broker and rebalance partitions.
Worker pool failsKeep accepting durably to Kafka, isolate workers with bulkheads, then restore processor state and catch up from the retained log.
Tournament end triggers a thundering herdPrecompute and cache the final board, rate-limit or stagger notifications, and have clients fetch bounded pages rather than repeatedly poll the whole result.

8. Alternatives when constraints change

Arbitrary filters and analytical queries

Filtered leaderboard path Filtered leaderboard pathFiltered leaderboard path
Flexible analytical filters use a separate query-oriented index while Redis can retain hot exact ranks.
  • Use this shape when queries combine country, game mode, time, percentile, and trends.
  • Feed a maintained OLAP or search store, such as ClickHouse, Apache Pinot, Elasticsearch, or OpenSearch, from the change stream. It adds ingestion lag and operational cost, but supports filters that a ZSET does not.
  • Keep Redis alongside the analytical path when hot exact-rank reads remain a requirement.

NoSQL leaderboard index: partition key and sort key

NoSQL partition and sort keys order records only within one partition. Use leaderboard + period + shard as the partition key and an encoded descending score + userId as the sort key, guarded by the same idempotency/version boundary as the ledger. Fixed or hash shards need a K-way merge for global Top K and cross-shard counts for exact rank; range shards make rank a sum of higher-band counts but risk hot bands and cross-band moves. None is a default replacement for one ZSET at this scale.

Approximate Top K at extreme scale

When exact rank is optional, a Count-Min Sketch (CMS) compresses score updates into a fixed-size grid of counters instead of storing every player separately. Each update touches a few counters selected from the player ID; the lowest matching value estimates that player’s score, while a size-K min-heap retains only the strongest candidates. Memory therefore stays bounded as the player population grows, which suits massive streams and trending boards. Hash collisions can inflate estimates, so verify finalists against an exact store when correctness matters.

Global and multi-region leaderboard

Choose the guarantee; regional partitioning does not create a globally consistent rank.

NeedDesignTrade-off
Low-latency local writesEach Region accepts immutable, idempotent events and serves a local projection; replicate asynchronously.Ranks can differ until replay completes; return asOf.
Eventual worldwide rankMerge all events into one canonical projection using global event IDs and versions.Correct after convergence, not at local-write latency.
Immediate exact worldwide rankRoute a board through one home region/sequencer or a cross-region quorum before acknowledging.Higher latency and reduced availability during a partition.

Replicate immutable award events, not mutable totals. Last-writer-wins replication can lose concurrent +10 and +20 updates; event-ID deduplication and serialized per-player aggregation make the merge correct. During a partition, choose stale/regional ranks, pause global-rank-affecting writes, or allow later corrections.

Multi-region NoSQL replication improves availability but does not create an atomic global rank: it can replicate individual records, not every player total and ordered-index update in one leaderboard transaction.

9. Common follow-up questions

What happens when the processor rebalances?

Kafka can redeliver work around a rebalance. The next processor instance restores its aggregate, version, and dedupe state from the compacted changelog, then resumes from committed offsets. Repeated event IDs are dropped within the dedupe horizon; repeated projection records are harmless because Redis and DynamoDB enforce their version/event-ID boundaries.

Why not use Redis alone?

Redis is excellent for the ordered index, but it is not the acceptance boundary. A retry of a relative increment can double-award and a cache-loss recovery needs a durable replay source. Kafka supplies that replay source; Redis projects absolute totals with versions.

How do we prevent a final-results traffic spike?

Close at a recorded watermark, wait until that watermark is projected, materialize the immutable final result, then serve it from cache or a CDN. Notifications are hints, not a signal for every client to poll the entire board.

Quiz

Leaderboard design challenge

Can you keep the rankings fast and correct?

Ten production scenarios. Choose the trade-off you can defend.

1 / 10
Question 1 of 10

Why is using ZADD with an absolute, statefully computed score version preferred over using relative score increments (ZINCRBY) directly in a Redis leaderboard projection?

Article navigation

Table of contents

Guided walkthrough

Video overview

Pressed for time—or merely negotiating with your attention span? Here’s the abridged video edition: same ideas, fewer paragraphs.