Skip to content

πŸ—οΈ Tic-Tac-Toe β€” High-Level Design

Target Level: Senior/Staff Engineer | Focus: Game architecture, AI, real-time, matchmaking


1. SYSTEM OVERVIEW

Purpose: Online Tic-Tac-Toe platform supporting PvP, AI, tournament, and NΓ—N board variants.

Scale: 500K MAU, 5K concurrent games, 100K games/day.

Users: Casual players, Competitive players, Spectators

Use Cases: Quick match, AI practice (3 levels), Tournament mode, NΓ—N board (4Γ—4, 5Γ—5)

Constraints: <50ms move latency, AI responds <1s, 99.9% uptime

Back-of-envelope: 100K games/day Γ— ~8 moves β‰ˆ 0.8M moves/day β‰ˆ 10 moves/s average; with a 10Γ— peak factor ~100 moves/s. 5K concurrent games Γ— a few hundred bytes of state β‰ˆ a few MB. Every number here fits on one small server; the design questions are about correctness (turn order, idempotent retries), reconnects, and not losing a finished game's result, not throughput. A staff answer says so and keeps the architecture small.


2. HIGH-LEVEL ARCHITECTURE

Web/Mobile App
      β”‚
β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
β”‚ API Gateway│── Rate Limiting ── Auth (JWT)
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
      β”‚
β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
β”‚ Matchmakingβ”‚  β”‚ Game Engineβ”‚  β”‚ AI Service β”‚
β”‚ (Go)       β”‚  β”‚ (Python)   β”‚  β”‚ (Python)   β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
      β”‚               β”‚               β”‚
      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
              β”‚    Redis       β”‚
              β”‚ (Game state,   β”‚
              β”‚  sessions, Q)  β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
              β”‚  PostgreSQL   β”‚
              β”‚ (users, games)β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Figure: services and stores behind the gateway.

flowchart TB
  C[Web / mobile app] --> G["API gateway: rate limit, JWT"]
  G --> M["Matchmaking (Go)"]
  G --> E["Game engine (Python)"]
  G --> A["AI service (Python)"]
  M --> R[("Redis: game state, sessions, queue")]
  E --> R
  A --> R
  R --> P[("PostgreSQL: users, games")]

🎬 Animated Sequence Diagram


🎬 Animated Tic-Tac-Toe Sequence β€” Player β†’ Move β†’ Win Check β†’ Board Update β†’ Turn Switch. Click β–Ά to play/pause. Created with Remotion.


3. KEY COMPONENTS

Game Engine (Python)

  • Board state w/ 3Γ—3 to NΓ—N support
  • Win detection: O(1) line sums when K = N; O(K) walk through the last move when K < N
  • Undo/redo w/ Command Pattern
  • Move validation chain

πŸ”΄ Interview Question: "How does your engine handle NΓ—N boards efficiently?"

βœ… Answer: Only lines through the last move can become a win, so never rescan the board. For K = N keep running sums per row/column/diagonal (+1 X, βˆ’1 O) and test abs(sum) == N: O(1) per move. For K < N walk the four directions from the last move: O(K). Storage is O(NΒ²) for the grid either way; a 1D array vs 2D is a constant-factor detail.


AI Service (Python)

  • Easy: Random legal move β€” O(NΒ²)
  • Medium: Minimax depth 3
  • Hard: Full minimax with a transposition table (optimal for 3Γ—3). Better still: 3Γ—3 has only 5,478 legal positions, so precompute the best move for every position once and ship the table (a few tens of KB). "AI" for 3Γ—3 becomes a hash lookup, and the AI service disappears for the classic board.

πŸ”΄ Interview Question: "How do you scale AI for 5Γ—5 boards?"

βœ… Answer: Exhaustive search is infeasible for 5Γ—5 (state space bounded by 3^25 β‰ˆ 8.5 Γ— 10ΒΉΒΉ). Use: 1. Monte Carlo Tree Search (MCTS): Simulate random playouts, select best node 2. Heuristic eval: Score based on lines controlled, center control 3. Time-bounded search: Return best move found within 2 seconds 4. Opening book: Pre-compute best responses to common first moves


Matchmaking (Go)

  • ELO-based pairing
  • Redis Sorted Set per skill bracket
  • Search expands Β±100 every 5 seconds

4. TRADE-OFFS

Decision Option A Option B Choice
Game state Server-authoritative Client-authoritative Server β€” prevents cheating
AI compute Server-side Client-side (WebAssembly) Server for complex, WASM for easy
Real-time WebSocket Polling REST WebSocket β€” <20ms latency
Board store In-memory (Redis) Database only Redis β€” fast reads, PG for persistence

5. SCALABILITY

Bottleneck: AI Service (CPU-bound for deep searches)

Solution: For 3Γ—3, a precomputed move table removes the bottleneck entirely. For NΓ—N, run time-boxed searches in a CPU worker pool behind a queue, with a per-request deadline so a slow search returns its best-so-far move. GPUs are not useful here; these are branchy tree searches, not tensor math.

Availability: 99.9%. Game servers hold no state that isn't in Redis/PostgreSQL. Redis replication + failover.


6. CORRECTNESS, IDEMPOTENCY & FAILURE MODES

Concern Design
Two moves for the same turn (double tap, two devices) One writer per game: the server applies make_move under a per-game lock (in-process) or a Redis Lua script that checks turn and move_number atomically
Client retries after a timeout Client sends move_number it expects to create. If the server already has that move from the same player, return the current state (idempotent); if it's a different move, reject as stale
Game server crash mid-game State lives in Redis (game:{id} hash: board string, turn, move_number, version). Any server can resume. Moves are also appended to PostgreSQL so a Redis failover can't lose a result
Result recorded twice (event redelivered) UNIQUE(game_id) on the results table; rating updates keyed by game_id
Player disconnects Hold the seat for N seconds; then forfeit by timeout. The timeout and a late move go through the same per-game writer, so exactly one wins
Redis down New games can't start; in-flight games pause. Finished results are already in PostgreSQL

Consistency choice: strong consistency within a game (single writer, ordered moves), eventual consistency for everything derived (leaderboards, stats, spectator views).

Figure: one move with an expected move_number, so client retries are safe.

sequenceDiagram
  participant C as Client
  participant E as Game engine
  participant R as Redis
  C->>E: move (game_id, move_number)
  E->>R: Load game state under per-game writer
  alt move_number already applied by same player
    E-->>C: Return stored result
  else next expected move
    E->>E: Validate and apply
    E->>R: Save state, move_number + 1
    E-->>C: New state
  end


7. COST (Monthly)

Component Cost
Game Engine (5 pods) $1,000
AI Service (CPU workers, NΓ—N only) $300
Redis + PostgreSQL $600
Total $1,900