To determine storage requirements, we need to track two types of data: move history, which allows users to replay past games, and active game state, which enables real-time gameplay updates.
Each move requires 100 bytes to store move details, timestamps, and player information. With 200 million moves per day, the daily storage requirement is:
For a full year, the total required storage is:
This data must be efficiently stored and indexed for fast retrieval while keeping long-term storage costs manageable.
For real-time gameplay, game states must be stored in memory to allow instant access. Given 5 million games per day, and assuming each game lasts 5 minutes, the system maintains around:
Each active game state consists of:
The total memory required to store all active games is:
This is well within the capacity of a Redis cluster, allowing fast game state retrieval and updates without significant memory pressure. The system can comfortably store and process all active game states in memory while persisting completed game data to long-term storage.
move(): Processes a player's move.
validateMove(): Validates the legality of a move based on chess rules.
matchPlayer(): Matches a player with an opponent based on ratings.
Using MongoDB for long-term storage makes sense given the scale and structure of the data. Since the system needs to handle millions of games and moves per day, a document-based NoSQL database provides flexibility and scalability over a traditional relational database. Unlike relational databases, which require schema modifications for changes, MongoDB’s document structure allows easy updates and efficient indexing for fast retrieval of match history and player data.
Redis is best suited for handling active game states and matchmaking queues because of its low-latency, in-memory storage. It manages high-frequency, short-lived data, including ongoing board states, player clocks, and move history. By keeping active game data in Redis, the system ensures instant retrieval and reduces database load.
Once a game is completed, MongoDB stores the final game record, move history, and player statistics. Since MongoDB scales horizontally, it can handle billions of games efficiently, supporting fast queries for leaderboards, match history, and player stats. This combination of Redis for real-time performance and MongoDB for long-term storage provides a scalable, high-performance architecture that balances speed, persistence, and flexibility.
Client Layer: The frontend communicates with the backend via WebSockets for real-time updates and REST/GraphQL APIs for user management and match history.
API Gateway & Load Balancer: The API Gateway routes requests, while a load balancer distributes WebSocket and API requests across multiple backend servers, ensuring high availability.
WebSocket Service: Handles persistent player connections, updates game state in Redis, and broadcasts real-time move updates to both players.
Game Service: Validates moves, checks for game-ending conditions, and updates Redis with the latest board state.
Matchmaking Service: Manage player queues based on Elo ratings, pairing players efficiently and preventing duplicate matches through atomic operations.
Redis: Stores ongoing game data (board state, clocks, moves) and matchmaking queues for fast lookup and updates.
PostgreSQL: Stores user profiles, completed match histories, and leaderboards.
Background Workers: Handle asynchronous tasks like rating updates, match archival, and leaderboard calculations.
POST /find-match).WebSocket: sendMove).GET /matches/{user_id}).WebSockets create a two way communication channel that stays open. This lets data move in both directions, which suits online chess. Polling needs repeated requests, adding overhead. Long polling reduces requests but still needs new connections to send data. By keeping one persistent connection, WebSockets are more efficient.
We need to handle both long term data, such as user accounts and match histories, and short term data, like active game states. A relational database such as PostgreSQL stores user profiles and match records, providing transactional guarantees and advanced querying. An in memory data store like Redis holds sessions and real time game states, offering fast reads and writes.
When a move happens, the service retrieves the current state from Redis, updates it, and notifies connected clients. Redis supports replication and pub sub for reliable, responsive gameplay. Once a match concludes, a background job writes the final state from Redis to PostgreSQL for permanent records. PostgreSQL can scale through replication and partitioning, while Redis can be deployed in a cluster. This approach combines durable storage for user and match data with the high performance necessary for real time interaction.
Each new player enters a queue with their current rating. The system searches for an opponent near that rating, and if none is found, it slowly widens the rating range to reduce wait times.
In practice, each waiting player’s rating is stored in a data structure like a sorted set in Redis. This allows the matchmaking service to perform quick range queries for similar ratings. When a match is made, both players are removed from the sorted set, and a game session starts. Over time, the service collects information about match outcomes, wait times, and rating distributions. It then refines the rating updates and how it expands the matching range, ensuring that players find balanced games without long delays.
To keep multiple instances from matching the same players more than once, Redis can use atomic operations or distributed locks.
Given the scale of the player base we will use a separate Redis cluster for the matchmaking queues. This prevents heavy queue operations from blocking or slowing down real-time state reads and writes.
Every time a player makes a move, the system must validate it against chess rules before updating the game state. This involves checking for piece movement legality, captures, check, checkmate, stalemate, castling rights, en passant, and insufficient material. Performing these calculations on every move can slow response times, especially under high traffic when thousands of games are active at once.
If move validation is not optimized, WebSocket responses can become delayed, impacting real-time gameplay. A poorly optimized system may also overload the game logic service, causing increased CPU usage and longer response times.
Forsyth–Edwards Notation (FEN) provides a compact, string-based representation of a chessboard's current state. Instead of recalculating every piece’s position from a database, the system stores the entire board in a single FEN string and retrieves it in constant time.
See Detailed Component Deisgn
When a WebSocket connection fails, the client detects the issue through the onclose or onerror event. If the connection drops due to a server crash, network issues, or a timeout, the client triggers a reconnection attempt. Instead of reconnecting immediately, the client uses exponential backoff, increasing the wait time between each attempt to avoid overwhelming the server. If the connection is restored, the client resets the retry counter and requests the latest game state from the server. The server retrieves the board position, move history, and clock data from Redis and sends it back to the client, allowing the player to continue without disruption. If multiple reconnection attempts fail, the client stops retrying and notifies the user. If a player remains disconnected for too long, the server may apply a game timeout and declare the opponent the winner. To prevent abuse, the server requires authentication before restoring a game session to ensure the correct player is reconnecting. This approach ensures that a dropped connection does not cause permanent game loss while maintaining fairness and stability.
A player may attempt to join matchmaking from multiple devices or browser sessions, or multiple matchmaking servers may process the same player simultaneously. Without proper synchronization, this can result in the same player being matched twice, causing inconsistencies and invalid game assignments.
When multiple matchmaking instances query Redis at the same time, they may retrieve the same player before removal. If both instances proceed with matchmaking, the player may end up in multiple games. To prevent this, distributed locks ensure that only one matchmaking instance can process a player at any given time.
Before assigning a player, the matchmaking service attempts to acquire a lock in Redis using SETNX. If successful, the server can proceed with matchmaking. If another instance has already locked the player, the request is ignored. The lock automatically expires after a short duration to prevent players from getting stuck due to server failures. This ensures a consistent, conflict-free matchmaking process, even with multiple servers running in parallel.
To improve scalability and fault tolerance, sharding Redis across regions can reduce load on a single instance.