ποΈ Chess Game β High-Level Design¶
Interviewer: Principal Software Engineer
Target Level: Senior/Staff Engineer
Focus: Game engine architecture, real-time multiplayer, AI integration
1. SYSTEM OVERVIEW¶
Purpose: Online chess platform supporting real-time multiplayer, AI opponents, game analysis, and tournament management.
Scale: launch at ~10K concurrent games / 50K games per day; design target below (100K concurrent, 500K games/day). The numbers in the rest of this doc use the design target.
Users: Players (casual/competitive), Spectators, Tournament organizers
Use Cases: - Play chess vs human (matchmaking) - Play chess vs AI (3 difficulty levels) - Analyze completed games - Watch live tournaments - Save and review game history
Constraints: <100ms move validation, <50ms state sync via WebSocket, <2s AI move generation (easy), 99.99% uptime for ranked play
Scale Targets: - 100K concurrent games (peak) - 1M+ registered users - 500K games/day - 1M+ WebSocket connections (players + spectators) - 50K matchmaking requests/minute - 200K game moves/minute (peak) β 3.3K moves/second
Back-of-envelope: 100K games Γ one move every ~30 s β 3.3K moves/s, consistent with the line above. Each move is ~100 bytes on the wire and one row in the move log. 500K games/day Γ ~80 plies β 40M move rows/day β 460 rows/s average. None of this is large for PostgreSQL; the hard parts are fan-out to ~1M sockets, clock accuracy, and never losing or double-applying a move.
2. HIGH-LEVEL ARCHITECTURE¶
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Applications β
β Web (React) β Mobile (React Native) β Desktop β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β HTTPS / WebSocket
ββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββ
β API Gateway β
β (Kong / AWS API Gateway) β
β - Authentication (OAuth2) β
β - Rate Limiting (100 req/s per user) β
β - WebSocket upgrade for live games β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β β β
βββββββββββΌβββββββ βββββββββΌββββββββ βββββββββΌββββββββ
β Matchmaking β β Game β β Analysis β
β Service β β Engine β β Service β
β (Go) β β (Python) β β (Python) β
βββββββββββ¬βββββββ βββββββββ¬ββββββββ βββββββββ¬ββββββββ
β β β
ββββββββββββββββββΌβββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β Kafka / Redis PubSub β
β (Game events, notificatons) β
ββββββββββββββββββ¬βββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β β β
βββββββββββΌβββββββ βββββββββΌββββββββ βββββββββΌββββββββ
β PostgreSQL β β Redis β β S3/Blob β
β - Users β β - Game state β β - Game β
β - Game historyβ β - Sessions β β replays β
β - Ratings β β - Match queueβ β - Analysis β
ββββββββββββββββββ βββββββββββββββββ βββββββββββββββββ
Figure: clients, gateway, services, event bus and stores.
flowchart TB
C["Clients: web, mobile, desktop"] -->|"HTTPS / WebSocket"| G["API gateway: auth, rate limit, WS upgrade"]
G --> M["Matchmaking (Go)"]
G --> E["Game engine (Python)"]
G --> A["Analysis (Python)"]
M --> K["Kafka / Redis PubSub"]
E --> K
A --> K
K --> P[("PostgreSQL: users, history, ratings")]
K --> R[("Redis: game state, sessions, queues")]
K --> S[("S3: replays, analysis")]
π¬ Animated Sequence Diagram¶
π¬ Animated Chess Game Sequence β Player β Move β Validation β Board Update β Game State. Click βΆ to play/pause. Created with Remotion.
3. COMPONENT BREAKDOWN¶
3.1 Game Engine Service¶
- Technology: Python (Python for chess logic; Go for real-time if needed)
- Responsibilities:
- Board state management (bitboard representation)
- Move validation (6 piece types, special rules)
- Check/checkmate/stalemate detection
- AI move generation (Minimax + Alpha-Beta pruning)
- Game timer management
π΄ Interview Question: "How would you architect the move validation to be fast enough for 10K concurrent games?"
β Answer: 1. Bitboard representation: Represent board as 12 Γ uint64 (one per piece type per color). Move generation becomes bitwise operations β 50M+ positions/second per core. 2. Pre-computed attack tables: Knight and king moves are lookup tables (O(1)). 3. Magic bitboards: Sliding pieces (bishop, rook, queen) use magic hash to pre-compute attack sets. 4. Validation pipeline: Move pattern check β legality check (king safety) β en passant/castling checks. 5. Stateless design: Each game state is self-contained (no shared state between games). Scale horizontally by adding more engine pods.
3.2 Matchmaking Service¶
- Technology: Go (high concurrency)
- Responsibilities:
- ELO-based opponent matching
- Rating range: Β±200 for fast matches, expanding over time
- Priority to same rating range first
- Handle casual vs ranked queues separately
π΄ Interview Question: "How does your matchmaking handle 10K users queueing simultaneously?"
β
Answer:
1. Redis sorted sets per rating bracket (e.g., 0-1000, 1000-1200, ...)
2. Each bracket key: matchmaking:{rating_bracket}
3. Users enter with score = ELO rating
4. Background worker polls each bracket: ZRANGEBYSCORE to find opponents within range
5. Match found β atomically remove both users, create game room
6. Expand search range every 5 seconds if no match found
Figure: matchmaking by rating bracket with a widening search range.
flowchart TD
A["User joins bracket sorted set (score = ELO)"] --> B["Worker: ZRANGEBYSCORE within range"]
B --> C{"Opponent found?"}
C -- Yes --> D["Atomically remove both, create game room"]
C -- No --> E["Wait 5 s, widen rating range"]
E --> B
3.3 Analysis Service¶
- Technology: Python + Stockfish integration
- Responsibilities:
- Position evaluation (centipawns, best move)
- Game annotation
- Blunder detection
- Opening book lookup
3.4 Data Layer¶
PostgreSQL Schema:
CREATE TABLE users (
id UUID PRIMARY KEY, username TEXT UNIQUE, elo_rating INT DEFAULT 1200
);
CREATE TABLE games (
id UUID PRIMARY KEY, white_id UUID, black_id UUID,
pgn TEXT, result TEXT, played_at TIMESTAMP DEFAULT NOW(),
time_control TEXT
);
CREATE TABLE moves (
id BIGSERIAL, game_id UUID REFERENCES games(id),
move_number INT, from_sq TEXT, to_sq TEXT, piece TEXT,
time_seconds INT
);
Redis:
- Active game state: HGETALL game:{id}:state
- User session: GET session:{token}
- Matchmaking queue: ZADD matchmaking:1500 {user_id} {rating}
S3 Storage: Game PGN exports, analysis reports, user profile images
3.5 Monitoring & Observability¶
| Metric | Tool | Alert |
|---|---|---|
| Move validation latency | Prometheus | p99 > 50ms |
| AI move generation | Prometheus | > 5s (easy mode) |
| Matchmaking time | Grafana | > 30s avg |
| Concurrent games | CloudWatch | > 80% capacity |
| WebSocket connections | Grafana | > 10K per pod |
4. TRADE-OFFS ANALYSIS¶
Trade-off 1: Server-side vs Client-side Game Logic¶
| Aspect | Server-side | Client-side |
|---|---|---|
| Trust | β No cheating possible | β Client can modify state |
| Latency | β 50-100ms per move | β <5ms |
| Cost | β Server compute | β Free |
| Complexity | More infrastructure | Simpler |
Decision: Server-authoritative for ranked games. Client-side for AI games (faster UX).
Trade-off 2: Bitboard vs Array Representation¶
| Aspect | Bitboard (uint64) | 2D Array |
|---|---|---|
| Speed | β 50M+ positions/s | β ~500K positions/s |
| Memory | ~96 bytes (12 Γ 64-bit) | ~64 bytes (one byte per square) β memory is not the reason to choose either |
| Complexity | β Harder to debug | β Simple |
| Move generation | β Bitwise ops | β Loops |
Decision: For a game server that validates one move per request, the array board is fast enough (well under a millisecond per legal-move check) and far easier to get right. Bitboards matter for the AI/analysis tier, which searches millions of positions; use a proven engine (Stockfish) there rather than writing one.
5. SCALING TO 100K CONCURRENT GAMES & 1M USERS¶
5.1 Architecture for Massive Scale¶
At 100K concurrent games and 1M users, the architecture must be fundamentally different from a simple monolith. Here's how each layer scales:
ββββββββββββββββββββββββββββββββββββββββ
β Global Load Balancer β
β (AWS Global Accelerator / Cloudflare)β
ββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββββΌββββββββββββββββββββββββββ
β API Gateway (Kong) β
β - Auth, Rate limiting, WebSocket β
β - Sticky sessions by game_id hash β
ββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β β β
ββββββββββββΌβββββββββββ ββββββββββββββΌββββββββββββ βββββββββββββΌβββββββββββ
β Matchmaking Service β β Game Engine Pods β β Analysis / AI β
β (Go, 3-5 pods) β β (Go, 50-200 pods) β β (Python, 10-30 pods)β
β - Redis ZSET queue β β - 1 game = 1 goroutineβ β - Async job queue β
β - Rating brackets β β - ~2000 games/pod β β - Stockfish workers β
β - Expand search β β - Bitboard engine β β - Spot GPU for AI β
ββββββββββββ¬βββββββββββ ββββββββββββββ¬ββββββββββββ βββββββββββββ¬βββββββββββ
β β β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β
ββββββββββββββΌββββββββββββββββββββββββββ
β Message Bus (Kafka) β
β Topics: game-events, rating-updates, β
β analysis-jobs, notifications β
ββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β β β
ββββββββββββΌβββββββββββ ββββββββββββββΌββββββββββββ βββββββββββββΌβββββββββββ
β PostgreSQL (Primary)β β Redis Cluster β β S3 / Object Store β
β - Aurora Global DB β β - 15+ shards β β - Game replays (PGN) β
β - 2 read replicas β β - Active game state β β - Analysis reports β
β - Cross-region DR β β - WebSocket pub/sub β β - User avatars β
β - Partitioned tables β β - Matchmaking queues β β β
ββββββββββββββββββββββββ βββββββββββββββββββββββββββ ββββββββββββββββββββββββ
5.2 Game Engine Placement & Architecture¶
Where does the game engine live?
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Game Engine Pod (Go) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Game Router β β
β β Routes WebSocket messages to the correct game loop β β
β ββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ β
β β Game Loop Manager β β
β β Manages 2000+ goroutines, each running one game β β
β β Each goroutine: β β
β β - Reads move from channel (blocking, no busy-wait) β β
β β - Validates move (bitboard engine, <100ΞΌs) β β
β β - Updates game state β β
β β - Publishes state change to Redis PubSub β β
β β - Flushes to PG every N moves (async) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββ¬ββββββββββββββββββββββββ¬ββββββββββββββββ β
β β Bitboard β Timer Manager β Redis Client β β
β β Engine β Per-game chess clocks β (go-redis) β β
β β (Go native) β O(1) timer heap β PubSub conn β β
β βββββββββββββββββ΄ββββββββββββββββββββββββ΄ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key design decisions:
1. **Game engine is in Go**, not Python β Go's goroutines make it trivial to manage 2000+ concurrent games per pod (each with its own timer, WebSocket connection, and game state). Python would require complex async patterns for similar throughput.
2. **One goroutine per game** β not per player. Both player WebSocket connections feed into the same game goroutine via channels. This eliminates all locking within a game.
3. **Single writer per game, enforced by a lease** β a pod owns a game while it holds a lease (e.g. `SET game:{id}:owner pod-7 NX PX 10000`, renewed every few seconds) and every write carries a fencing token (the lease epoch). If a pod crashes, another pod takes the lease, rebuilds the game from the durable move log, and resumes. Without fencing, a paused-but-alive old owner could write after the new owner took over (split brain).
4. **Bitboard engine compiled as native Go** β no CGo, no Python overhead. Pure Go bit manipulation for move generation.
Game Engine Code Architecture (Go):
// Game represents a single chess game running in a goroutine
type Game struct {
ID string
WhitePlayer string
BlackPlayer string
Board *Bitboard // Current bitboard state
MoveHistory []Move
WhiteClock *ChessClock
BlackClock *ChessClock
Status GameStatus
// Channels for communication
MoveChan chan PlayerMove // Receives moves from players
StateChan chan GameState // Publishes state changes
DisconnectChan chan string // Player disconnected
Done chan struct{} // Game over signal
}
func (g *Game) Run() {
timer := time.NewTicker(100 * time.Millisecond) // Timer resolution
defer timer.Stop()
for {
select {
case move := <-g.MoveChan:
// Reject duplicates / stale submissions: the client echoes the ply it saw
if move.ExpectedPly != len(g.MoveHistory) {
g.StateChan <- GameState{Error: "Stale move", Player: move.Player}
continue
}
if !g.Board.IsLegalMove(move.Player, move.From, move.To) {
g.StateChan <- GameState{Error: "Illegal move", Player: move.Player}
continue
}
// Persist BEFORE acknowledging: INSERT ... (game_id, move_number) is the PK,
// so a second writer for the same ply fails instead of forking the game.
if err := g.Store.AppendMove(g.ID, len(g.MoveHistory)+1, move, g.LeaseEpoch); err != nil {
g.StateChan <- GameState{Error: "Retry", Player: move.Player}
continue
}
g.Board.ApplyMove(move.From, move.To, move.Promotion)
g.MoveHistory = append(g.MoveHistory, move.ToMove())
g.SwitchTurn()
if g.Board.IsCheckmate() || g.Board.IsStalemate() || g.Board.IsAutomaticDraw() {
g.EndGame() // persists the result, then publishes game_over
return
}
// Publish new state to Redis PubSub (-> other player + spectators)
g.PublishState()
// Note: never hand g.MoveHistory to another goroutine (e.g. `go flush(g.MoveHistory)`)
// while this loop keeps appending to it: that is a data race. Copy it, or don't.
case playerID := <-g.DisconnectChan:
g.HandleDisconnect(playerID)
case <-timer.C:
// Tick chess clocks
g.TickClocks()
case <-g.Done:
return
}
}
}
5.3 State Management & Board Positions¶
Three-tier state architecture:
| Tier | Technology | Purpose | Recovery |
|---|---|---|---|
| L1 - Hot | Redis (in-memory) | Active game state, FEN, timers, last 50 moves | Cache; rebuilt from L2 on loss |
| L2 - Warm | PostgreSQL (game_sessions + game_moves) | Source of truth: every move, written before ack | Replay the move log |
| L3 - Cold | S3 / Object Store | PGN exports, archived games (older than 6 months) | Bulk restore |
State serialization format:
{
"game_id": "abc-123",
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"turn": "black",
"status": "active",
"white_player": {
"id": "user-1",
"time_ms": 600000,
"last_move_at": 1704067200
},
"black_player": {
"id": "user-2",
"time_ms": 598000,
"last_move_at": 1704067210
},
"move_count": 1,
"last_moves": ["e2e4"],
"checkpoint_version": 0,
"started_at": 1704067190
}
Persistence strategy:
- The move log in PostgreSQL is the source of truth. Each accepted move is one INSERT into game_moves with PK (game_id, move_number) before the move is acknowledged. At ~3.3K moves/s peak this is comfortably within one primary (or a few hash-partitioned tables).
- Redis is a hot cache of the derived state (FEN, clocks) for fast reconnects and spectators. Losing it loses nothing: replay the move log.
- Batching every 10 moves (a common suggestion) means a pod or Redis failure can silently delete up to 10 moves of a ranked game. Unacceptable for a game whose result changes ratings; per-move writes are cheap enough that batching isn't worth it.
- Zombie game cleanup: TTL on Redis keys (max 24h per game). Games exceeding max time (e.g., unlimited time control β 7 days) auto-draw.
5.4 WebSocket Architecture for 1M+ Connections¶
Challenge: 100K active games Γ 2 players + ~8 spectators/game (avg) = ~1M concurrent WebSocket connections.
Connection Architecture:
Client A ββWebSocketβββ ββββββββββββββββββββββββ
Client B ββWebSocketβββ β WebSocket Gateway β βββββββββββββββββββ
Client C ββWebSocketβββ β (Go, 10 pods) ββββΆ Redis PubSub β
Client D ββWebSocketβββ β - Connection mux β β game:{id}:state β
... β - Per-pod 100K connsβ ββββββββββ¬βββββββββ
1M connections β - Graceful drain β β
β - Rate limiting β β
ββββββββββββββββββββββββ β
β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββΌββββββββββββ
β Game Engine Pods β
β (Go, 50-200 pods) β
β Subscribe to Redis β
β PubSub for their β
β assigned games β
βββββββββββββββββββββββ
Key WebSocket design decisions:
-
WebSocket Gateway is separate from Game Engine β a thin proxy that maintains 1M+ connections. Each gateway pod can handle ~100K connections (Go's goroutines are lightweight: ~5KB per connection).
-
Routing to the owning pod: a gameβpod lookup (the lease key above, cached at the gateway) or consistent hashing on
game_id. Plainhash(game_id) % num_podsreshuffles almost every game when a pod is added or removed, which is exactly when you can least afford mass migration. -
Redis PubSub for broadcasting: When engine pod processes a move, it publishes the new state to Redis PubSub channel
game:{id}:state. The gateway pod (which subscribed to this channel via Redis) pushes the update to both players' WebSocket connections. -
Connection migration: If a gateway pod fails, clients reconnect via a global connection registry (Redis SET). The new gateway subscribes to the game's Redis channels and resumes pushing state.
-
Spectator mode: Spectators subscribe to the same Redis PubSub channel. The gateway fan-out pattern means adding 100K spectators costs almost nothing β just more WebSocket connections on the gateway tier.
WebSocket message protocol:
// Client β Server (move submission)
{
"type": "move",
"payload": {
"from": "e2",
"to": "e4",
"promotion": null,
"expected_ply": 0
},
"timestamp": 1704067200000
}
// Server β Client (state update)
{
"type": "state",
"payload": {
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"turn": "black",
"status": "active",
"white_time_ms": 598000,
"black_time_ms": 600000,
"move_count": 1,
"last_move": "e2e4"
},
"timestamp": 1704067200010
}
// Server β Client (game over)
{
"type": "game_over",
"payload": {
"result": "white_win",
"reason": "checkmate",
"final_fen": "...",
"winner_rating_change": +15,
"loser_rating_change": -15
}
}
// Server β Client (clock sync)
{
"type": "clock_sync",
"payload": {
"white_time_ms": 450000,
"black_time_ms": 600000,
"server_time": 1704067500000
}
}
// Sent every 500ms to keep clocks in sync
5.5 Processing Simultaneous Games at Scale¶
Challenge: 200,000 moves/minute peak. How do we validate, store, and broadcast efficiently?
Move processing pipeline:
1. Client sends move via WebSocket
β
2. WebSocket Gateway receives
β - Validate message format
β - Rate limit check (max 3 moves/sec per player)
β - Forward to correct engine pod via Redis PubSub
β
3. Engine Pod processes move
β - Validate move legality (bitboard, <100ΞΌs)
β - Apply move to board
β - Update chess clocks
β - Check for check/checkmate/stalemate
β - Publish new state to Redis
β - Append move to PG move log (before ack)
β
4. Redis PubSub fan-out
β - All gateway pods subscribed to this game's channel
β - Gateways push state to player WebSockets + spectators
β
5. Background services react
β - Rating service: update ELO after game ends
β - Analysis service: queue Stockfish analysis
β - Notification service: push move alerts
β - Anti-cheat: scan for suspicious patterns
Throughput calculations:
| Component | Cost per move | At 3.3K moves/s | Notes |
|---|---|---|---|
| Move validation (array board, Python) | ~0.5 ms CPU | ~1.7 cores | Bitboards would cut this ~100Γ; not needed |
| PG move-log insert | ~1β3 ms latency | 3.3K inserts/s | One well-provisioned primary; hash-partition game_moves for vacuum/index size, not for throughput |
| Redis state update + PUBLISH | <1 ms | ~6.6K ops/s | A single Redis node handles ~100K ops/s; shard for memory/fan-out, not ops |
| WebSocket pushes | ~10 recipients per move (2 players + ~8 spectators) | ~33K pushes/s | Spread across gateway pods; popular games need a separate fan-out path |
Bottleneck management:
-
Clock sync traffic, not moves, dominates Redis/WebSocket load.
clock_syncevery 500 ms Γ 100K games Γ 10 recipients = 2M messages/s. Send clock state only with moves (clients count down locally from the server timestamp) and resync every few seconds, not every 500 ms. -
Hot games: a top-board tournament game with 50K spectators. Don't fan out from the engine pod: publish once to a dedicated channel and let a CDN-style relay tier (or SSE with caching) handle viewers, who tolerate a second of delay.
-
WebSocket broadcast storm: When 100K games all produce moves simultaneously, gateways could get overwhelmed. Solution: Rate-limit state broadcasts per-game (max 10/s). If a game produces moves faster (premoves in bullet chess), batch the last known state.
5.6 Anti-Cheat & Fair Play¶
| Threat | Detection | Mitigation |
|---|---|---|
| Engine assistance | Analyze move correlation with Stockfish top moves. Flag >90% correlation. | Shadow-ban: match suspected cheaters only with each other. Manual review for ranked. |
| Automated play (bots) | Mouse movement analysis, move timing patterns (sub-second moves duration), CAPTCHA challenges. | Rate limit moves/sec. Require periodic verification. |
| Sandbagging | Detect intentional rating drop by analyzing game patterns (blunders in winning positions). | Rollback ELO manipulation. Temporary ban. |
| Collusion (tournaments) | Graph analysis of opponent matching patterns. IP address clustering. | Disqualify. Publish transparency report. |
| DDoS via game creation | Rate-limit game creation per user (max 1 concurrent game). | CAPTCHA on account creation. Tiered rate limits based on account age. |
5.7 Database Scaling Strategy¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL (Aurora Global) β
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Primary β β Read β β Read β β
β β (us-east-1) ββββ Replica 1 ββββ Replica 2 β β
β β - Writes β β - Game readsβ β - Analytics β β
β β - Game β β - User β β - Leaderboardβ β
β β creation β β profiles β β - History β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β β
β β Cross-region replication β
β ββββββββΌβββββββ β
β β DR (eu-west) β β
β β - Standby β β
β βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Redis Cluster (15+ shards) β
β β
β Shard 0: game:0* β game:6** β Shard 1: game:7*...β
β Shard 2: user sessions β Shard 3: matchmakingβ
β Shard 4-14: more game shards β β
β β
β Each shard: 1 primary + 2 replicas (for HA) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Partitioning strategy for game_moves:
- Partition by game_id hash (mod 64) β evenly distributes write load
- Sub-partition by month for easy archival
- Older partitions can be moved to slower (cheaper) storage or S3
Read optimization for game replay:
- Recently finished games (last 7 days): served from Redis cache
- Older games: served from PostgreSQL (game_moves partitioned by month)
- Archived games (>6 months): served from S3 as pre-generated PGN files
6. SCALABILITY & RELIABILITY¶
Availability: 99.99% (ranked) / 99.9% (casual)
Game State Recovery:
1. Every move is in PostgreSQL before it is acknowledged (see 5.3)
2. Redis persistence (AOF every 1s + RDB every 5min) only shortens warm-up; it is not relied on for correctness
3. If Redis fails completely β replay game_moves for each active game (β80 rows per game; seconds for the whole fleet)
Disaster Recovery:
- Cross-region replica of PostgreSQL (Aurora Global Database)
- On full region failover, active games resume from the replicated move log. Aurora Global replicates asynchronously (typically under a second of lag), so a region loss can drop the last move or two; the client re-submits with its expected_ply and the server either accepts or returns the authoritative state. Clocks are paused for the failover window.
- Redis replication across AZs within region
Graceful degradation: | Condition | Degraded behavior | |-----------|-------------------| | Redis cluster partially down | Games on affected shards pause (show "reconnecting" to players). Other games unaffected. | | PG primary down | Read replica promoted (15-30s failover). Game creation paused. Existing games continue from Redis. | | Engine pod crash | Games on crashed pod detected by heartbeat timeout (5s). Redis state loaded, new pod spawns, games resume. | | Full game service down | Active games paused. Rating changes deferred. All games resume when service is back. |
Scaling: - Game engine pods: Horizontal based on active game count (target ~2K games per pod, one goroutine per game; scale out at ~80% of that) - WebSocket gateway pods: Scale based on connection count (target 100K connections/pod) - AI computation: GPU instances with auto-scaling queue. Analysis jobs queued via Kafka, processed as capacity allows. - Redis cluster: Add shards as memory usage crosses 70% - PostgreSQL: Read replicas for leaderboard queries, analytics offloading
Idempotency & ordering (summary)¶
| Risk | Guard |
|---|---|
| Client retries a move after a timeout | Client sends expected_ply; server returns the current state if the ply already advanced |
| Two pods both think they own a game | Lease + fencing epoch stored with each move write; the store rejects lower epochs |
| Same ply written twice | PRIMARY KEY (game_id, move_number) on game_moves |
| Rating applied twice when the game-over event is redelivered | UNIQUE (user_id, game_id, rating_type) on rating history; consumer is idempotent |
| Flag-fall timer races a last-second move | Both go through the game's single writer; whichever is processed first wins |
Figure: one move; it is durable in PostgreSQL before the ack, then fanned out.
sequenceDiagram
participant C as Client
participant W as WebSocket gateway
participant E as Game engine
participant P as PostgreSQL
participant R as Redis PubSub
C->>W: move (expected_ply)
W->>E: Forward move
E->>E: Validate (pattern, king safety)
E->>P: Insert game_moves (game_id, move_number)
E-->>C: Ack
E->>R: Publish game state
R-->>W: State event
W-->>C: Push to players and spectators
7. COST BREAKDOWN (Monthly at Full Scale)¶
| Component | Cost (100K games) | Cost (10K games) | Notes |
|---|---|---|---|
| Game Engine (50-200 pods) | $15,000 | $4,000 | c5.xlarge, auto-scaling |
| WebSocket Gateway (10 pods) | $3,000 | $1,000 | c5.2xlarge, ~100K conns/pod |
| Matchmaking (3-5 pods) | $600 | $400 | Small instances |
| PostgreSQL Aurora (Global) | $4,000 | $1,200 | serverless scaling |
| Redis Cluster (15 shards) | $6,000 | $600 | r6g.large per shard |
| AI computation (Stockfish) | $2,000 | $800 | Spot GPU + CPU mix |
| Kafka + Event Bus | $1,500 | $500 | MSK or self-hosted |
| CDN + Bandwidth | $2,000 | $500 | 10TB egress |
| S3 Storage | $500 | $200 | Game replays + analysis |
| Monitoring (Datadog/Prom) | $1,500 | $500 | Metrics, traces, logs |
| Total | $36,100 | $9,700 |
8. IMPLEMENTATION ROADMAP¶
Phase 1 (Month 1-2): Core game engine with all rules. Bitboard in Go. Local multiplayer only. Basic CLI test.
Phase 2 (Month 3-4): Online multiplayer with WebSocket. Redis for game state. Simple matchmaking (ELO-based). Game history in PostgreSQL.
Phase 3 (Month 5-6): AI depth 6 with alpha-beta. Game analysis (Stockfish). Async job queue (Kafka). Batching: 100 concurrent games.
Phase 4 (Month 7-8): Scale to 10K concurrent games. Redis Cluster. PG read replicas. Auto-scaling engine pods. Tournaments.
Phase 5 (Month 9-12): Scale to 100K concurrent games. Global deployment (multi-region). Spectator mode. Anti-cheat. Advanced analytics.