Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Serverless Leaderboard on AWS

A real-time, multi-period leaderboard API built entirely on managed AWS services — API Gateway, Lambda, and DynamoDB — with an EC2/Nginx dashboard on the front end. No Redis, no sorted-set hacks, no servers to patch: DynamoDB's own index does the sorting.

Architecture diagram

Players → EC2/Nginx dashboard → API Gateway → 5 Lambda functions → DynamoDB (us-east-1)

What's in this repo

File Route Job
lambda/score-submit.py POST /scores Writes a score into all-time, daily-*, and weekly-* in one call, keeping the higher of the new and existing score for each ("high-score-wins").
lambda/leaderboard-query.py GET /leaderboard Queries the rank GSI for a period (?period=all-time|daily|weekly) and returns the top N, already sorted — no app-side sort.
lambda/player-stats.py GET /player Looks up one player's entries across every leaderboard they're on and computes live rank + percentile for each.
lambda/leaderboard-snapshot.py POST /snapshot Freezes the current top-N of a leaderboard into a history table, timestamped, for later trend/audit views.
lambda/score-simulator.py POST /simulate Batch-generates realistic players and scores across all three periods — the seed data behind the screenshots below.

All five functions run under one shared IAM execution role scoped to the two DynamoDB tables and CloudWatch Logs.

The core trick: inverted-score GSI

DynamoDB's GSI sort key only reads efficiently in one direction (ascending). Rather than scan-and-sort in application code, the score is inverted at write time so an ascending index query returns players in descending rank order for free:

MAX_SCORE = 999999
SCORE_PAD = 7

def make_inverted(score, player_id):
    """Convert real score to inverted sort key for descending order in GSI."""
    inv = MAX_SCORE - int(score)
    return f'{str(inv).zfill(SCORE_PAD)}#{player_id}'
Player Real score Stored inverted_score
CosmicWolf 9,011 0990988#cosmicwolf
BlazeMaster 8,851 0991148#blazemaster
EpsilonEdge 8,357 0991642#epsilonedge

Zero-padding to 7 digits keeps the string comparison numerically correct; appending player_id guarantees a unique, tie-broken key even when two players finish with identical scores. leaderboard-query.py then just does:

table.query(
    IndexName='leaderboard-rank-index',
    KeyConditionExpression=Key('leaderboard_id').eq(leaderboard_id),
    ScanIndexForward=True,   # ascending inverted = descending real score
    Limit=limit,
)

High-score-wins writes

A single submission fans out to every active leaderboard period, but only ever raises a score:

for lb_id in get_leaderboard_ids():  # all-time, daily-*, weekly-*
    existing = table.get_item(Key={'player_id': player_id, 'leaderboard_id': lb_id}).get('Item')
    current = int(existing.get('score', 0)) if existing else 0

    new_score = max(int(score), current)  # high-score-wins
    table.put_item(Item={..., 'score': new_score, 'inverted_score': make_inverted(new_score, player_id)})

Rank and percentile, computed live

No stored rank column to keep in sync — a player's position is derived on read by counting how many inverted scores beat theirs:

rank_resp = table.query(
    IndexName='leaderboard-rank-index',
    KeyConditionExpression=Key('leaderboard_id').eq(lb_id) & Key('inverted_score').lt(inv_score),
    Select='COUNT',
)
rank = rank_resp.get('Count', 0) + 1
percentile = round(((total - rank) / total) * 100, 1)

Data model

leaderboard-scores — the live, mutable state

  • PK player_id, SK leaderboard_id
  • GSI leaderboard-rank-index on leaderboard_id + inverted_score

leaderboard-snapshots — immutable, append-only history

  • Keyed by snapshot_id = {leaderboard_id}#{timestamp}

Live dashboard

Leaderboard dashboard

38 players, top 25 returned in 293ms via the rank GSI — All-Time / Today / This Week tabs, live query timing, and one-click snapshot/seed controls for demos.

Lambda console

All five functions deployed and independently scalable (Python 3.14, zip package).

Concepts exercised

  • Inverted-score GSI — descending rank from an ascending index, no Redis sorted set needed
  • High-score-wins writes — read-then-conditional-max on every submit protects existing bests
  • Multi-period fan-out — one submission updates all-time, daily, and weekly leaderboards per-item
  • Derived percentile — rank and percentile computed on read via COUNT queries, never stored or allowed to drift
  • Point-in-time snapshots — historical leaderboard states captured on demand into an append-only table
  • CORS-enabled REST API — every Lambda handles its own OPTIONS preflight for the browser dashboard

Where this pattern applies

Gaming leaderboards & tournaments · sales performance tracking · educational quiz platforms · fitness & health rankings · employee performance dashboards · social engagement metrics — anywhere you need a ranked, high-score-wins view over a large, frequently-updated set of scores.

Stack

API Gateway (REST, 5 routes) · Lambda (Python 3.14, 5 functions) · DynamoDB (2 tables) · EC2 + Nginx (dashboard) · IAM (single shared execution role) · CloudWatch (logs)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages