Understanding the Problem
An online chess service allows two players of roughly comparable strength to discover each other, play the same match in real time, and update their ratings after the game finishes. During play, the backend acts as the authority for both the board state and the clocks. Every move must be validated by the server, and time cannot be trusted to either client.
If you're not familiar with competitive chess, each player controls one side of the board and takes turns making moves. Both players also have separate countdown timers determined by the chosen time control. Some games may last well over an hour, while faster formats such as blitz or bullet give each player only a few minutes—or even seconds—to work with. In those faster games, network delay becomes part of the system-design problem because a small amount of latency can materially affect how much time a player appears to consume.
Players also have ratings that approximate skill level. Those ratings help the matchmaking system find balanced opponents and determine each player's position relative to the broader player population.
We will begin with the mechanics of hosting a single live chess game correctly. Once that foundation is in place, we can scale outward to the harder distributed-system problems: matching large numbers of players, operating many concurrent game servers, and keeping time measurement fair when the two players experience different network conditions.
Functional Requirements
-
Skill-Based Matchmaking: Players should be able to enter a matchmaking queue and be paired with an opponent of roughly similar rating. Once a match is found, the system should create a new game and connect both players to it.
-
Real-Time Gameplay: Two players should be able to play the game interactively, with moves and clock updates propagated to both sides with low latency. The server should remain authoritative over move legality, game state, and time remaining.
-
Ratings and Leaderboard: After a game ends, the system should update the players' ratings and reflect those changes on a global leaderboard. Players should also be able to view their own current rank shortly after the result is finalized.
Non-Functional Requirements
-
Low-Latency Move Delivery: A valid move should reach the opponent quickly, with an end-to-end target below 200 ms under normal conditions. This is especially important for blitz and bullet games, where players may have only a few seconds to think. Noticeable delay in move propagation makes the board and clock feel unresponsive.
-
Prefer Consistency for Active Game State: The server should maintain one authoritative version of the board and clocks. If the game server becomes temporarily unavailable, it is better to pause or interrupt the match than allow both clients to continue independently and diverge into conflicting states. A delayed game can usually be resumed; a corrupted game state is much harder to repair correctly.
-
Support Large Numbers of Concurrent Matches: At peak, the platform should handle roughly 500,000 simultaneous games, which corresponds to about 1 million active player connections. The architecture therefore needs to scale both real-time connection handling and game-state processing horizontally.
Data Model
-
Player: Represents a registered user of the platform. The player record contains identity information along with a chess rating, such as Elo or a similar rating score. This rating is used both to find opponents of comparable strength and to determine the player's position on the leaderboard.
-
Game: Represents one match between two players. It records which player controls White and Black, the current board position, whose turn it is, the remaining time on both clocks, the selected time control, and the final result once the game ends.
-
Move: Represents a single action within a game. A move may include the source square, destination square, move sequence number, server timestamp, and any special metadata such as promotion. Moves are stored as an append-only history so that the complete game can later be replayed, audited, or reconstructed if needed.
-
MatchRequest: Represents a player's temporary entry in the matchmaking system before an opponent has been found. It includes information such as the player's current rating and desired time control—for example, a
3+2game with three minutes of starting time and a two-second increment after each move. Match requests are kept separate from games because matchmaking needs its own lifecycle: a request can wait, expand its acceptable rating range, be cancelled, or eventually be paired with another compatible request to create a new game.
API Design
This system has two different interaction patterns. Matchmaking and leaderboard queries are short-lived request/response operations, so REST is a natural fit. A live chess game is different: both players continuously exchange moves and clock updates with the server, which is better modeled as a persistent WebSocket connection.
We can therefore divide the interface into a REST control plane and a WebSocket gameplay protocol.
Matchmaking
A player enters matchmaking by creating a new MatchRequest. The only information the client needs to provide is the desired time control:
POST /matchmaking Body: { "timeControl": "rapid-10-0" } Response: { "matchRequestId": "mr_8421", "status": "SEARCHING" }
The matchmaking request is processed asynchronously. Once a compatible opponent is found, the system creates a Game and notifies both players. Importantly, the client does not send its own playerId or rating. The gateway derives the player's identity from the authenticated session or JWT:
auth token | v playerId | v Player record | v current rating
This prevents clients from manipulating matchmaking inputs. If a player could submit:
{ "playerId": "alice", "rating": 900 }
a stronger player could simply claim a lower rating to receive easier opponents. Any value that affects competitive fairness—identity, rating, account status, or matchmaking tier—should come from trusted server-side state.
Live Gameplay
After matchmaking creates a game, both players establish a WebSocket connection scoped to that game: WS /games/{gameId}. The WebSocket carries the real-time gameplay protocol. A client may send for example
For example:
sendMove { from: "g1", to: "f3", moveNumber: 7 }
The server validates the move against the authoritative game state before committing it. It can then respond to the player who submitted the move:
moveAck { accepted, reason?, whiteTimeMs, blackTimeMs }
If accepted, the opponent receives:
opponentMove { from, to, moveNumber, whiteTimeMs, blackTimeMs }
When the game finishes, both players receive:
gameEnd { result }
The result may indicate a win, loss, draw, resignation, timeout, or another terminal condition. The exact WebSocket message names are not important. Unlike REST, there is no universal convention for representing application-level socket events. What matters in an interview is clearly defining which messages travel in each direction and making the server authoritative over state transitions.
Leaderboard and Player Rank
Leaderboard access is read-heavy and fits naturally behind REST. Clients can page through globally ranked players:
GET /leaderboard?cursor={cursor}&limit={limit} Response: { "players": [...], "nextCursor": "..." }
A player may also want their own rank without scanning leaderboard pages:
GET /players/me/rank Response: { "rank": 18427, "rating": 1763 }
Using /players/me/rank keeps the same trust boundary as matchmaking—the server derives the player identity from authentication rather than allowing the caller to specify an arbitrary user ID.
Together, these interfaces separate the system cleanly: REST handles matchmaking and leaderboard queries, while WebSockets carry the latency-sensitive stream of moves, clock updates, and game events.
High-Level Design
Players Should Be Able to Find a Similar-Skill Opponent and Start a Game
We begin with the first thing a player needs: an opponent. The matchmaking path takes a player's request, searches for another player with compatible preferences and a similar rating, and creates the Game that will later be handed to the real-time gameplay system.
For the initial design, we introduce two pieces:
- Matchmaking Service: Accepts players into the matchmaking pool, selects compatible pairs, and creates a new game when a match is found.
- MatchRequest Table: Stores players currently waiting for a game, including their server-side rating, desired time control, and matchmaking status.
Basic Matchmaking Flow
The simplest version works like this:
- The client calls the Matchmaking Service and specifies the desired time control.
- The service obtains the player's current rating from trusted server-side data and inserts a new request:
MatchRequest - playerId - rating - timeControl - status = PENDING
-
The matcher searches the table for another
PENDINGrequest with the same time control and a sufficiently close rating. For example, a player rated1650might initially search within:1450 <= opponentRating <= 1850 -
If a compatible request is found, the service claims both requests, creates a
Game, and marks the two matchmaking entries asMATCHED. The resulting game might contain:
Game - gameId - whitePlayerId - blackPlayerId - timeControl - status = ACTIVE
Both clients then receive the gameId and use it to establish their gameplay WebSocket connection.
Waiting for an Opponent
One subtle detail is how the client learns that a match has been found. For this first version, the matchmaking request behaves like a long-poll. The HTTP request does not immediately return after the MatchRequest row is inserted. Instead, the server keeps it open while the player remains in the matchmaking pool. Suppose Player A enters the queue first. Their request stays open:
Later, Player B arrives and is compatible with Player A. The matcher pairs them, creates the game, and completes both outstanding matchmaking requests:
Player A long-poll ----+ | +--> Game created | Player B long-poll ----+
Both players therefore receive the match result through the request they already opened. We do not need a separate push-notification channel just to tell a waiting player that an opponent has appeared.
Expanding the Search Range
Restricting matchmaking to a fixed rating range can create very long waits, especially for players near the top or bottom of the rating distribution. Instead, the acceptable range can widen as waiting time increases.
For example:
0-10 sec: rating +/- 100 10-20 sec: rating +/- 200 20-30 sec: rating +/- 350