# System Design: [Gaming leaderboard name]

> Reference design for a real-time gaming leaderboard: score updates from
> trusted game servers, ranking with in-memory sorted sets, deterministic tie
> breaking, "my rank" and "top N" queries, sharding when one set is not
> enough, friends leaderboards, seasonal resets, and the boundary with
> anti-cheat. It is an introductory design: it explains each building block
> before using it, so it suits a first system design review or interview
> practice. It follows the structure of the System Design Document template,
> adds a 45-minute interview path, access-pattern and failure-mode tables, and
> is pre-filled with a worked design to adapt. Replace every `[bracketed]`
> value and every row marked *Example*; values marked *Example* are
> assumptions, not benchmarks.

<!--
Guidance: How to use this template
- Decide first who is ranked against whom: everyone in the game, players in a
  region, a friends list, or a tournament bracket. Each scope is a separate
  leaderboard with its own key.
- Write the tie rule before writing code. "Who reached the score first wins"
  and "alphabetical" give different answers and players notice.
- Keep the source of truth outside the cache. The sorted set is fast but is
  rebuilt from durable score records after any loss.
- Treat cheating as an input problem: the leaderboard accepts scores only
  from trusted servers; detection lives in a separate anti-cheat system.
-->

## Document control

<!-- Guidance: A design is a proposal until it is approved; record who decided and when. -->

| Field | Value |
|---|---|
| Document ID | [SDD-LB-001] |
| System / feature | [Real-time leaderboard] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| Difficulty | Introductory |
| Games and modes covered | [Game titles, modes, regions] |
| Related | [Match service design], [Anti-cheat policy], [ADR: tie rule] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Game design (ranking rules) | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Trust and safety (anti-cheat) | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Note what changed in the design, not the wording. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Encoded time into the score to break ties by first to reach |
| 0.3 | YYYY-MM-DD | [@name] | *Example:* Added friends leaderboard computed at read time |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page that a busy reviewer can read alone: problem, proposed solution in a paragraph, the decisions that need attention, and the short interview path. -->

### 1.1 Problem statement

[*Example:* The game computes rankings with a nightly SQL job that sorts all
players by points. Players see yesterday's rank, the "top 100" page times out
during events, and two players with equal points swap places between page
loads. The new season mode needs live ranks for 50 million players.]

### 1.2 Proposed solution

[*Example:* When a match ends, the match service (a trusted server, not the
game client) publishes a signed score event. A leaderboard writer records the
event durably in a database and updates an in-memory sorted set for the
current season. Ranks and the top 100 are read from the sorted set in a few
milliseconds. Ties are broken by who reached the score first, encoded inside
the sorted-set score. Friends leaderboards are computed at read time from the
friend list. Each season uses a new key, so a reset is a key switch, not a
mass delete.]

### 1.3 Decisions requested from reviewers

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Database ORDER BY or in-memory sorted set? | Sorted set, database as source of truth | 7.1, 10 |
| Q-02 | *Example:* How are ties broken? | First to reach the score ranks higher | 7.2 |
| Q-03 | *Example:* Shard now or later? | One primary with replicas now; shard plan ready | 7.3 |
| Q-04 | *Example:* Who can submit scores? | Only the match service, with signed events | 7.6 |
| [Q-nn] | [Question] | [Option] | [Section] |

### 1.4 45-minute interview path

Use this order when the document serves an interview or a short review.

| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Scope: update score, top N, my rank, friends; season reset; below-the-line items | Four operations |
| 5 to 10 | Estimates: updates per second, players per board, memory | Three numbers, "fits in one node" consequence |
| 10 to 15 | API: submit score (server only), get top, get my rank with neighbours | Three calls |
| 15 to 25 | High-level design: match service, writer, database, sorted set, read API | One diagram |
| 25 to 40 | One deep dive: why a database sort fails and how a sorted set works (default), or ties, or sharding | Options, choice, reason |
| 40 to 45 | Failures (cache lost, cheating), 10x change | Failure rows |

Start the deep dive with the naive answer, a database query that counts rows
with a higher score, and show why it gets slower as players grow. Then
introduce the sorted set and its logarithmic rank lookup.

## 2. Requirements

<!-- Guidance: Split core requirements from below-the-line items so the design stays focused. Non-functional targets need numbers and a way to measure them. -->

### 2.1 Core functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* Add points to a player's season score when a match ends | Must |
| FR-02 | *Example:* Show the top 100 players of the season | Must |
| FR-03 | *Example:* Show a player's rank and the 5 players above and below | Must |
| FR-04 | *Example:* Break ties deterministically: first to reach the score ranks higher | Must |
| FR-05 | *Example:* Start a new season monthly, keep past seasons readable | Must |
| FR-06 | *Example:* Show a leaderboard of the player's friends | Should |
| FR-07 | *Example:* Remove a banned player and restore ranks | Must |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Read latency | *Example:* p99 at or below 50 ms for top 100 and my rank | API timing |
| NFR-02 | Freshness | *Example:* New score reflected in rank within 2 s of match end | Event timestamp to read |
| NFR-03 | Durability | *Example:* No accepted score lost, even if the cache is lost | Rebuild drill |
| NFR-04 | Availability | *Example:* 99.9% for reads; writes may queue during incidents | Probes |
| NFR-05 | Correctness | *Example:* Same rank shown to every viewer at the same moment | Consistency test |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |

### 2.3 Below the line (out of scope)

- [*Example:* matchmaking ratings such as Elo or Glicko (a separate service; the leaderboard can display them)]
- [*Example:* cheat detection itself (anti-cheat system; see 7.6 for the boundary)]
- [*Example:* rewards and prize payout at season end]
- [*Example:* cross-game global rankings]
- [Item deferred, with the release that will take it]

## 3. Capacity estimation

<!-- Guidance: Every number must end in a consequence for the design. Show inputs, formulas and results; round generously and say which way you rounded. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Daily active players | $U$ | *Example:* 25 million | Analytics |
| Matches per player per day | $m$ | *Example:* 10 | Analytics |
| Leaderboard views per player per day | $v$ | *Example:* 20 | Client telemetry |
| Players in one season board | $P$ | *Example:* 50 million | Monthly actives |
| Memory per sorted-set entry including overhead | $e$ | *Example:* 100 bytes | Measured with 16-byte ids |
| Peak to average ratio (events, weekends) | $k$ | *Example:* 5 | Traffic history |
| Simple operations one cache node serves per second | $c$ | *Example:* 100 000 | Benchmark on target instance |

### 3.2 Score updates

$$
W = \frac{U \times m}{86\,400} = \frac{2.5 \times 10^{8}}{86\,400} \approx 2\,900 \ \text{updates/s}, \qquad W_{peak} \approx 5 \times 2\,900 = 14\,500 \ \text{updates/s}
$$

Consequence: the write rate is far below what one in-memory node handles,
so sharding is not needed for writes. Writes go through a queue only to
absorb spikes and to make retries safe.

### 3.3 Reads

$$
R = \frac{U \times v}{86\,400} = \frac{5 \times 10^{8}}{86\,400} \approx 5\,800 \ \text{reads/s}, \qquad R_{peak} \approx 29\,000 \ \text{reads/s}
$$

The top 100 is the same for everyone, so it is cached in the API for 1 s.
"My rank" is per player and goes to the sorted set. Consequence: about
$14\,500 + 29\,000 \approx 44\,000$ operations per second at peak, under half
of one node's *Example* capacity $c$; read replicas add headroom and
availability.

### 3.4 Memory

$$
M = P \times e = 5 \times 10^{7} \times 100 \ \text{B} = 5 \ \text{GB per season board}
$$

Consequence: the current season fits on one node with room to spare. Past
seasons move to the database and are not kept in memory.

### 3.5 Summary

| Quantity | Estimate | Design consequence |
|---|---|---|
| Score updates | ~14 500/s at peak | One primary; queue for spikes and retries |
| Reads | ~29 000/s at peak | Cache top 100 for 1 s; replicas for my-rank |
| Memory | ~5 GB per season board | One node; shard only for much larger boards (7.3) |
| Durability | Cache can be lost | Database is the source of truth; rebuild job |

## 4. API design

<!-- Guidance: Define the contract first: resources, methods, request and response shapes, errors, idempotency, pagination, rate limits and versioning. Use RFC 9457 problem details for errors. -->

### 4.1 Endpoints

| Interface | Operation | Caller | Purpose |
|---|---|---|---|
| Internal event | `ScoreAwarded{match_id, player_id, points, season, ts, signature}` | Match service only | Add points |
| Public API | `GET /v1/leaderboards/{board}/top?limit=100` | Client | Top N |
| Public API | `GET /v1/leaderboards/{board}/players/{player_id}?around=5` | Client | Rank and neighbours |
| Public API | `GET /v1/leaderboards/{board}/friends` | Client (signed in) | Friends ranking |
| Admin | `POST /v1/leaderboards/{board}/players/{player_id}/remove` | Trust and safety | Remove a banned player |

### 4.2 Request and response examples

A rank request for one player with neighbours, and the response:

```http
GET /v1/leaderboards/season-2026-10/players/p_8812?around=2 HTTP/1.1
```

```json
{"board": "season-2026-10", "player": {"player_id": "p_8812", "rank": 1042, "points": 18350}, "neighbours": [{"rank": 1040, "player_id": "p_1201", "points": 18360}, {"rank": 1041, "player_id": "p_5530", "points": 18350}, {"rank": 1043, "player_id": "p_0077", "points": 18340}, {"rank": 1044, "player_id": "p_3109", "points": 18335}], "as_of": "2026-10-22T09:30:01Z"}
```

Players p_5530 and p_8812 both have 18 350 points; p_5530 reached it first,
so it ranks higher (7.2).

### 4.3 Errors and idempotency

| Case | Status | Body type |
|---|---|---|
| Unknown board or season | 404 | `problems/unknown-board` |
| Player has no score this season | 200 with `rank: null` | Not an error |
| Score event with bad signature | Rejected, logged for trust and safety | Not visible to clients |
| Too many requests | 429 with `Retry-After` | `problems/rate-limited` |

Score events carry `match_id`. The writer records `(season, match_id,
player_id)` with a unique constraint, so a replayed event adds points once.

## 5. Data model

<!-- Guidance: Choose storage by access pattern, not by habit. Show entities, keys, partitioning and the query each store must answer. -->

### 5.1 Entities

The database holds durable records; the sorted set is a derived index.

```mermaid
erDiagram
  SEASON {
    string season_id PK
    datetime starts_at
    datetime ends_at
    string status
  }
  SCORE_EVENT {
    string season_id PK
    string match_id PK
    string player_id PK
    int points
    datetime awarded_at
  }
  PLAYER_SCORE {
    string season_id PK
    string player_id PK
    bigint points
    datetime reached_at
    bool removed
  }
  FINAL_STANDING {
    string season_id PK
    string player_id PK
    int final_rank
  }
  SEASON ||--o{ SCORE_EVENT : "records"
  SEASON ||--o{ PLAYER_SCORE : "totals"
  SEASON ||--o{ FINAL_STANDING : "archives"
```

### 5.2 Storage choices

| Data | Store | Key | Why |
|---|---|---|---|
| Score events | *Example:* Relational database, partitioned by season | `(season_id, match_id, player_id)` | Idempotency and audit |
| Player totals | Same database | `(season_id, player_id)` | Source for rebuild and season archive |
| Live ranking | *Example:* Redis sorted set | `lb:{game}:{season}` | Rank and range in logarithmic time |
| Final standings | Database table per season | `(season_id, final_rank)` | Past seasons without memory cost |
| Friend lists | Social service | `player_id` | Owned elsewhere, read at query time |

### 5.3 Access patterns

| Query | Key or index that serves it | Partition key and rationale |
|---|---|---|
| Top 100 of the season | `ZRANGE lb:{game}:{season} 0 99 REV` | Board key; one board fits one node |
| My rank | `ZREVRANK` on the board key with the player id | Same board key |
| Players around me | `ZRANGE ... REV` from rank minus 5 to rank plus 5 | Same board key |
| Friends ranking | `ZMSCORE` for up to a few hundred friend ids, sort in the API | Board key; friends are a small set read at query time |
| Rebuild after loss | Scan `PLAYER_SCORE` by `season_id` | Season partition; sequential read |
| Past season rank | `FINAL_STANDING` by `(season_id, player_id)` | Season partition; cold data |

## 6. High-level design

<!-- Guidance: One diagram that shows every component on the request paths and where state lives. -->

```mermaid
flowchart LR
  game([Game client]) --> match[Match service, trusted]
  match -->|signed ScoreAwarded| q[[Score event queue]]
  q --> writer[Leaderboard writer]
  writer --> db[(Score database)]
  writer --> zset[(Sorted set primary)]
  zset --> rep[(Read replicas)]
  game --> api[Leaderboard API]
  api --> cache[Top-N cache, 1 s]
  api --> rep
  api --> social[Social service: friends]
  ac[Anti-cheat system] -->|remove or adjust| writer
  rebuild[Rebuild job] --> db
  rebuild --> zset
```

| Component | Responsibility | Scales by |
|---|---|---|
| Match service | Decide results and points on the server | Game fleet |
| Score event queue | Buffer spikes, retry safely | Partitions by player id |
| Leaderboard writer | Verify signature, record event, update totals and sorted set | Instances, one partition each |
| Sorted set primary and replicas | Rank and range queries | Replicas; shards in 7.3 |
| Leaderboard API | Read paths, top-N cache, friends merge | Stateless instances |
| Rebuild job | Recreate the sorted set from the database | On demand |

## 7. Detailed design

<!-- Guidance: Go deep on the parts that are new, risky or contested. For each deep dive list the options, the choice and the reason. -->

### 7.1 Deep dive: why a sorted set

To find a player's rank, something must know how many players have a higher
score.

| Option | Top N | My rank | Problem |
|---|---|---|---|
| Database `ORDER BY points DESC LIMIT 100` | Fast with an index | `COUNT(*) WHERE points > mine` scans many index entries | Rank cost grows with the number of players above you |
| Batch job that writes ranks nightly | Fast | Fast | Hours old |
| In-memory sorted set (skip list plus hash map) | Logarithmic | Logarithmic | Memory bound; needs a durable source |

A sorted set keeps members ordered by a numeric score. Adding or updating a
member and looking up its rank both take time proportional to the logarithm
of the set size, so 50 million players cost only a few dozen steps. In Redis
the commands are `ZADD` or `ZINCRBY` to write, `ZREVRANK` for rank from the
top, and `ZRANGE ... REV` for a slice by rank (`ZREVRANGE` is deprecated since Redis 6.2).

**Choice:** a sorted set per board, with the database as the source of
truth. **Reason:** it is the only option that serves both top N and my rank
in milliseconds for a live board.

```mermaid
sequenceDiagram
  participant M as Match service
  participant W as Writer
  participant D as Database
  participant Z as Sorted set
  participant C as Client
  M->>W: ScoreAwarded(match_id, player, +40)
  W->>D: insert event (unique), update total, reached_at
  D-->>W: new total 18350, reached_at
  W->>Z: ZADD lb:g1:2026-10 composite(18350, reached_at) p_8812
  C->>Z: ZREVRANK p_8812 (via API and replica)
  Z-->>C: rank 1042 (zero-based 1041)
```

The writer writes the absolute total computed by the database rather than
incrementing in the cache. A retried write then sets the same value again
instead of adding points twice.

### 7.2 Deep dive: ties

When scores are equal, Redis orders members by their names byte by byte
(reversed for the `ZREV` commands). The order of two tied players then
depends on their ids, which is not a game rule.

| Option | Rule | Cost |
|---|---|---|
| Accept lexicographic order | Arbitrary but stable | Players see unfair order |
| Shared rank ("both are 1041st") | Equal scores share a rank | Rank query must count distinct higher scores; more work |
| Encode time in the score | Earlier achiever ranks higher | Score range must fit the encoding |

**Choice:** encode time. Redis scores are double-precision numbers, which
represent integers exactly only up to $2^{53}$. Split those 53 bits into
points and time: with $t$ the whole seconds since the season started,

$$
\text{composite} = \text{points} \times 2^{22} + \left(2^{22} - 1 - t\right)
$$

$2^{22}$ seconds is about 48.5 days, enough for a monthly season, which
leaves 31 bits for points (up to about 2.1 billion). Earlier $t$ gives a
larger composite, so the earlier achiever ranks higher. The API decodes
points as $\lfloor \text{composite} / 2^{22} \rfloor$. **Reason:** it keeps
every query a single sorted-set call. Players who reach the same score in
the same second still fall back to id order; document that as the final
rule.

### 7.3 Deep dive: sharding and top N

Sharding is not needed at the estimated size (3.5), but plan it for boards
that outgrow one node, for example a global all-time board.

| Option | Top N | My rank | Risk |
|---|---|---|---|
| Shard by player id hash into $K$ sets | Read top N from each shard, merge $K \times N$ results | $1 + \sum_{k} \text{ZCOUNT}_k(\text{score} > s)$ across all shards | Rank needs $K$ calls |
| Shard by score range (buckets) | Read the highest bucket | Sizes of higher buckets plus rank inside own bucket | Hot top bucket; players move between shards |
| Exact rank for the top only, percentile for the rest | Exact for top 10 000 | "Top 3%" from a score histogram | Approximate for most players |

**Choice:** hash sharding when needed, plus percentile display below the top
10 000. **Reason:** hash sharding keeps load even and top N is a cheap merge;
players outside the top rarely need an exact six-digit rank, and a histogram
answers "top 3%" with one lookup.

### 7.4 Friends leaderboards

| Option | How | Cost |
|---|---|---|
| Fan out on write | Update a sorted set per friend on every score | Each update writes hundreds of sets |
| Compute on read | Fetch friend ids, read their scores with one multi-get, sort in the API | One social call plus one cache call per view |

Choose compute on read: friend lists are small (cap at *Example* 500), and
friends boards are viewed far less often than scores change. Cache the
result per player for *Example* 10 s.

### 7.5 Seasons and resets

Each season has its own key, for example `lb:{game}:2026-10`. At the season
boundary (defined in UTC), the writer switches to the new key; the old set
stays read-only for *Example* 7 days for late disputes, then a job writes
`FINAL_STANDING` and deletes the old key. No mass delete happens at
midnight, and late events for the old season are rejected by the season
check. All-time boards, if any, are separate keys updated in parallel.

### 7.6 Anti-cheat boundary

The leaderboard is not where cheating is detected, but its design can make
cheating harder:

- Accept scores only from the match service, never from game clients, and
  verify an event signature or service identity.
- Reject events outside plausible ranges (for example more points than a
  match can award) and route them to review.
- Support removal: the anti-cheat system calls the admin API, the writer
  marks the player removed in the database and removes the member from the
  sorted set; ranks of everyone below change immediately.
- Keep every score event, so points can be recomputed after a rules change
  or an exploit.

### 7.7 How real systems do it

> **How real systems do it** (paraphrased, not copied). The AWS Database
> Blog post "Build a real-time gaming leaderboard with Amazon ElastiCache for
> Redis" (2019, updated 2024) describes moving ranking out of a relational database into Redis
> sorted sets, so that each score write keeps the set ordered and rank
> queries no longer scan tables. A later AWS for Games post on Amazon
> MemoryDB discusses the durability gap of a cache-only leaderboard and the
> common pairing of an in-memory store with a persistent database, which is
> the split used in this design. The Redis documentation itself lists
> leaderboards as a typical use of sorted sets and states that equal scores
> are ordered lexicographically, which is why 7.2 exists. Read the originals
> in 12.3; this box keeps only the general idea.

## 8. Scalability and performance

<!-- Guidance: Show how the design meets the numbers in section 3 and where it will break first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Score writes | One primary, queue in front | *Example:* about 50% of node capacity |
| Top-N reads | 1 s cache in the API | None in practice |
| My-rank reads | Read replicas | Replica lag above NFR-02 |
| Board size | One node per board | *Example:* above 30 GB, shard by player hash (7.3) |
| Events (launch day) | Queue absorbs bursts; writer scales with partitions | Queue lag above 2 s |

Latency budget for "my rank with neighbours" (target p99 50 ms):

| Step | Budget |
|---|---|
| API, auth | 10 ms |
| `ZREVRANK` on a replica | 2 ms |
| `ZRANGE ... REV` for neighbours | 2 ms |
| Profile names for 11 players (cached) | 10 ms |
| Network and headroom | 26 ms |

## 9. Reliability, security and operations

<!-- Guidance: Name the failure modes with their blast radius and recovery time, the threats, and the signals operators watch. -->

### 9.1 Failure modes

| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Sorted set primary | Node lost | Health check | Writes for the board pause | Promote replica; replay recent events from the queue | *Example:* 1 min |
| Sorted set data | Lost or corrupted | Count check against database | Board shows wrong ranks | Rebuild job from `PLAYER_SCORE` | *Example:* 15 min for 50 million entries |
| Writer | Crashes mid-batch | Queue lag | Freshness only | Idempotent events, absolute totals | *Example:* 1 min |
| Database | Primary failover | Error rate | Writes queue; reads unaffected | Queue holds events until it returns | *Example:* 5 min |
| Season switch | Wrong boundary or timezone | Events rejected spike | One season's scores | UTC boundary in config, rehearsed in staging | *Example:* 30 min |
| Exploit | Inflated scores | Anti-cheat signals, outlier alerts | Top of the board | Remove, recompute from events | *Example:* 1 h |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [Time] |

### 9.2 Security and privacy

| Threat | Control |
|---|---|
| Client submits its own score | No public write endpoint; only the match service publishes events |
| Forged events on the queue | Signed events or authenticated producer identity |
| Scraping of player lists | Rate limits; public names only, no account ids |
| Display of minors' names | Show display names chosen under the game's child safety rules |
| Abuse in display names | Name moderation before a player can appear in public boards |
| [Threat] | [Control] |

### 9.3 Observability

| Signal | Metric | Alert threshold |
|---|---|---|
| Freshness | Match end to rank update, p99 | Above 2 s for 10 min |
| Consistency | Sorted-set size vs database count | Difference above 0.01% |
| Read latency | p99 per endpoint | Above 50 ms |
| Suspicious scores | Points per match above the game maximum | Any |

### 9.4 Cost estimate

| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Cache nodes (primary and 2 replicas) | [USD per node-month] | 3 | [USD] |
| Database | [USD per month] | 1 cluster | [USD] |
| Queue | [USD per month] | [Partitions] | [USD] |
| **Total** | | | **[USD]** |

## 10. Trade-offs and alternatives

<!-- Guidance: For each major decision list the options, what each costs and why you chose one. Include what changes at ten times and at one tenth of the load. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Ranking store | Database sort; batch; sorted set | Sorted set plus database | Live ranks in milliseconds | Two stores to keep in sync |
| Tie rule | Lexicographic; shared rank; time encoded | Time encoded | Fair, one call per query | Score range limited to 31 bits of points |
| Write style | Increment in cache; set absolute total | Absolute total from database | Retries are safe | Database on the write path |
| Friends board | Fan out on write; compute on read | Compute on read | Small lists, rare views | Extra call per view |
| Season reset | Delete members; new key | New key | Instant, no mass delete | Old keys to clean up |
| What changes at 10x / 0.1x | 10x players: shard by player hash and show percentiles below the top. 0.1x: a database index with a 10 s cached top N is enough | Review per game launch | Size moves the cache from optional to required | [Note] |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |

## 11. Delivery plan and risks

<!-- Guidance: How the design reaches production safely, and what could stop it. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Writer, database, sorted set, top 100 | Load test at 2 times peak | YYYY-MM-DD |
| M2 | My rank, neighbours, tie rule | Tie tests pass | YYYY-MM-DD |
| M3 | Seasons and archive | Season switch rehearsed | YYYY-MM-DD |
| M4 | Friends board, removal flow | Removal reflected within 2 s | YYYY-MM-DD |

### 11.2 Rollout and rollback

[*Example:* Run the new leaderboard in shadow for one week, comparing ranks
with the nightly job for a sample of players. Switch the client to the new
API behind a flag; rollback switches the flag back to the nightly ranks.]

### 11.3 Risks

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* Points exceed 31 bits after a scoring change | Low | High | Validate in the writer; widen time unit or shorten season | [@name] |
| R-02 | *Example:* Players dispute tie order | Medium | Low | Publish the rule in game help | [@name] |
| R-03 | *Example:* Exploit floods the top before detection | Medium | High | Plausibility limits, fast removal tool | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

## 12. Appendices

<!-- Guidance: Supporting detail, glossary, references and open questions. -->

### 12.1 Open questions

| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | *Example:* Regional boards as separate keys or filtered views? | [@name] | YYYY-MM-DD |
| OQ-02 | *Example:* Do removed players keep their final standing record? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |

### 12.2 Glossary

| Term | Meaning |
|---|---|
| Sorted set | A collection of unique members ordered by a numeric score |
| Rank | Position of a player counted from the top, starting at 1 in the UI |
| Composite score | Points and time packed into one number so ties sort correctly |
| Season | A time-boxed competition with its own board |
| Percentile rank | Share of players with a lower score, shown instead of an exact rank |

### 12.3 References

| Reference | Use |
|---|---|
| Redis documentation, "Redis sorted sets" (redis.io) | 7.1 commands, 7.2 lexicographic tie order |
| Tan, "Build a real-time gaming leaderboard with Amazon ElastiCache for Redis", AWS Database Blog, 2019 (updated 2024) | 7.7 |
| Ishii, "Rethinking Game Leaderboards with Amazon MemoryDB for Redis", AWS for Games Blog, 2022 | 7.7 durability discussion |
| MDN Web Docs, `Number.MAX_SAFE_INTEGER` | 7.2 exact integers up to 2 to the power 53 |
| Xu and Lam, *System Design Interview: An Insider's Guide, Volume 2* (2022), chapter "Real-time Gaming Leaderboard" | Inspiration only; no text or figures reused |
| Distributed Key-Value Store System Design (`system-design/design-distributed-kv-store`) | Replication background |
| Back-of-Envelope Estimation and Capacity Model (`system-design/back-of-envelope-estimation`) | Method for section 3 |

## Standard and licence note

Based on common industry practice for real-time ranking and on the public
documentation cited in 12.3. The book and engineering write-ups listed there
are inspiration only; no text, diagrams or numbers were copied, and all
figures here are illustrative examples. Check the current documentation of
your cache and database for command behaviour and limits. Original text
written for this collection. Template licence: MIT.
