Designing Data-Intensive Applications
Case 7

Merging Moves and Leaderboards

Concurrent edits to shared boards need versioning; leaderboards need approximate top-K at scale.

Diagram

Step-by-step walkthrough

Play path — validate and persist

  • ① POST /move — Player submits a move; server is authoritative, not the client.
  • ② UPDATE WHERE version=N — Optimistic lock on board row; stale version rejected with 409 Conflict.
  • ③ Append move event — Immutable move log supports replay, spectators, and audit.

Score path — leaderboard and timers

  • ④ ZADD score — Redis sorted set updated on valid move; top-100 is O(log N + 100).
  • ⑤ Trigger forfeit — Scheduler fires when turn timer expires; API applies forfeit rule.
  • ⑥ GET top-100 — Leaderboard reads from Redis, not a full table scan in Postgres.
Diagram
typescript — Optimistic board version check
// Reject stale concurrent moves
async function applyMove(gameId: string, version: number, move: Move) {
  const updated = await db.games.updateMany({
    where: { id: gameId, version },
    data: { board: applyRules(move), version: { increment: 1 } },
  });
  if (updated.count === 0) throw new Error("STALE_BOARD — refresh and retry");
}
In practice

Chess.com stores PGN move logs; lichess uses similar event-sourced game records. NYT Games leaderboard uses precomputed daily ranks to avoid scanning all players at read time.

Why these technologies?

Why PostgreSQL (board state)?

Turn-based games tolerate 50–200 ms latency — ACID updates with version column prevent stale moves. Simpler than CRDTs for single-writer-per-turn chess.

Why Append-only move log?

Replay games, spectator mode, and dispute resolution. Event sourcing lite — board is a projection of moves.

Why Redis sorted sets (leaderboard)?

O(log N) rank updates and top-K queries at millions of players. Recomputing ranks in SQL nightly is too slow for live daily puzzles.

Why Scheduler (SQS delay / Redis TTL / cron)?

Forfeit players who exceed turn timeout without polling every second in application code.

Why HTTPS / WebSocket (not UDP)?

Puzzle games are not latency-competitive — reliable delivery matters more. JSON payloads are tiny.

Why Push notifications (APNs/FCM)?

Async turn games notify opponent it's their move — optional but standard for retention.

Key Takeaways
  • Board state version increments on each valid move; stale versions rejected.
  • Shared puzzles (collaborative crosswords) may use CRDT grids for cells.
  • Leaderboards: Redis sorted sets for real-time; batch reconcile for seasons.
  • Anti-cheat: server-side dictionary and move legality only.
  • Spectator mode reads immutable move log — same pattern as event sourcing.
  • Tie-breakers defined upfront (time, fewer hints).
leaderboardversioningRedisevent sourcingCRDTPostgreSQLQPS