Design Chess

Design a chess LLD: board, pieces, legal moves, specials, and the check filter that stops Alice from leaving her king in danger.

Page content

Alice (White) develops her knight. Bob’s king is in check. The hop is a clean 2-by-1.

If move() only asks “do knights go 2-by-1?” and not “is my own king safe after this?”, she can castle through check or leave herself in check. The game is already illegal. Geometry is not chess.

This is an LLD interview: a board, six piece types, a move that is legal only when Alice’s king is safe afterward. It is not Stockfish, not a WebSocket lobby, not a chess.com clone. Same muscle as Design a Parking Lot or Design Snake and Ladder — objects and a state machine, not a multiplayer product.

The main design question is:

How do we represent the board, generate legal moves (including specials), and decide check, checkmate, and stalemate?

We will start with a switch on piece type and watch promotion explode it. Strategies, a king-safety filter, castling rights, and a small state machine appear when that switch is no longer enough.

1. Clarify the problem

“Design chess” can mean an engine that plays, a realtime app with clocks and chat, or a rules object two humans share. The questions that change the objects:

  • Standard chess, or variants (960, crazyhouse)?
  • Two human players, or an AI side?
  • Do we need a clock? Increment? Flag fall?
  • Undo? PGN export? A UI, or just move()?
  • Draw by agreement, 50-move, threefold repetition?
  • One game in memory, or a server of rooms?

If the interviewer gives no extra constraints, I would state:

Rules               FIDE standard, one game
Players             Two: Alice White, Bob Black
Variants            Out of scope
Engine / eval       Out of scope (not Stockfish)
Network             Out of scope (not sockets, not a lobby)
Clocks              Optional wrapper; not part of legality
Draw extras         Name 50-move and threefold; do not implement
Undo                Nice if the move log is rich; not required for v1

A chess server with matchmaking is HLD. Say so and stay on the board. Kafka does not belong in this picture.

2. Functional requirements

The system must:

  1. Start a game from the standard position, White to move.
  2. Accept a move from the side whose turn it is, or reject it.
  3. List legal moves for the side to move (for a UI, or for mate tests).
  4. Detect check, checkmate, and stalemate after every legal move.
  5. Allow resign.
  6. Handle the specials: pawn double-step, en passant, castling, promotion.

Not in v1: an evaluation function, opening books, bitboards, a clock product, takebacks as a social feature, 50-move or threefold as implemented draws. Those sit on the same Board and Move. They do not change isLegal.

3. Non-functional requirements

This is not a QPS problem. Interviewers want correct rules, not a cluster.

RequirementTarget
CorrectnessEvery accepted move is legal FIDE chess
SpeedMilliseconds; 64 squares, ~20–40 legal moves
ConcurrencyOne game; serialize turns, do not “CAS the knight”
HistoryMove list is enough for undo and a later PGN
ClockServer time if they insist; never the client

A wrong castle is a failed interview. A 2 ms move generator is a hobby. Prefer an 8×8 array you can draw on the whiteboard over bitboards you cannot debug in fifteen minutes.

Edge cases

  • Alice moves a piece that does not get her king off a check.
  • Alice castles through a square Bob attacks, or out of check.
  • A pinned bishop “can” slide — until the king-safety filter.
  • Pawn on the 7th rank: four promotions, or a capture-promotion.
  • En passant is legal only on the turn after a double-step.
  • Bob captures Alice’s rook and she loses that castle right.
  • move is called when it is not that side’s turn.
  • move after CHECKMATE or RESIGNED.
  • Two legal escapes from check; Alice picks neither.

4. Scale

Say the numbers so nobody spends the interview on throughput.

Squares             64
Pieces              32 at start, fewer later
Legal moves / turn  typically 20–40, not millions
Games               one object in this interview
Players             two

Generating every pseudo-legal move and throwing away those that leave the king in check is O(pieces × directions × 8) plus one “make / unmake / is the king safe?” per candidate. That is fine. An engine’s alpha-beta search is a different problem. If they say “ten thousand rooms,” each room is this Game. You still do not introduce a message bus.

Retention is the move log, not a data warehouse. Keep it for undo and PGN. You do not need a replay cluster.

5. Entities / what we store

Six facts, and a derived status.

Color          WHITE | BLACK
PieceType      KING QUEEN ROOK BISHOP KNIGHT PAWN
Square         file 0–7 (a–h), rank 0–7 (1–8)
Piece          color, type          (or a subclass per type)
Move           from, to, promotion?, flags, captured?
Board          8×8 cells, side to move, castle rights, en passant square
Game           board, status, move log

A move is the source of truth for “what changed.” The board is the position you can see. Status (CHECK, CHECKMATE, …) is derived after the move lands: recompute check and whether the opponent has a legal reply. If status and the legal-move list disagree, the list wins.

Board: Alice king e1, Bob rook e8, e-file empty
  Alice tries  Nb1-c3
  filter: after Nc3, e1 is still attacked
  reject

Do not store “Alice is in check” as a flag you set by hand. Compute it. Castle rights and the en passant target are real state — they are not recoverable from the piece grid alone.

Squares look like e4 in speech and (file=4, rank=3) in code (0-based). Pick one encoding and convert at the edge.

6. APIs or class methods

In LLD you show a service, not HTTP. A chess “REST API” is a UI adapter.

start() -> Game
move(from, to, promotion?) -> MoveResult
legalMoves(color?) -> [Move]
resign(color) -> Game
status() -> GameStatus

Start

Request:

start()

Response:

{ "status": "IN_PROGRESS", "to_move": "WHITE", "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" }

FEN is optional flavor. The object is a Game, not a string.

Move

move({ "from": "b1", "to": "c3" })

Accepted:

{ "move": "Nc3", "status": "IN_PROGRESS", "to_move": "BLACK", "in_check": false }

Illegal (leaves own king in check):

{ "error": { "code": "ILLEGAL_MOVE", "message": "Own king would be in check" } }

Wrong side is NOT_YOUR_TURN. Already over is GAME_OVER. A pawn on the 8th rank without promotion is PROMOTION_REQUIRED (or you default to queen — say which). The UI highlights legalMoves(). It must not invent a castle the filter rejected.

7. Start with a switch in Game — watch it explode

The first design everyone writes:

function move(from, to):
  piece = board.at(from)
  if piece.color != sideToMove: throw NOT_YOUR_TURN
  switch piece.type:
    KNIGHT:
      if not isTwoByOne(from, to): throw ILLEGAL
    BISHOP:
      if not isClearDiagonal(from, to): throw ILLEGAL
    PAWN:
      if not isOneForward(from, to): throw ILLEGAL
    ...
  board.put(to, piece)
  board.clear(from)
  flipSide()

It is easy to draw and easy to break.

    a b c d e f g h
  8 . . . . r . . k
  7 . . . . . . p p
  6 . . . . . . . .
  5 . . . . . . . .
  4 . . . . . . . .
  3 . . . . . . . .
  2 . . . . . . P P
  1 . N . . K . . R

Alice to move. Bob’s rook on e8 owns the e-file.

Nb1-c3 is a knight hop. The switch says yes. After the hop, e1 is still attacked. Alice has left herself in check. The position is illegal.

The same switch has nowhere honest to put:

  • promotion (four piece types appear on to);
  • en passant (the captured pawn is not on to);
  • castling (two pieces move; three squares must be safe);
  • pins (the bishop’s diagonal is clear, and still illegal).

You will add if on if until Game is the rulebook and every new special touches every case. That is the parking-lot naive scan: it looks like the domain and does not protect the invariant.

switch in Game              wrong (and it grows)
geometry only               wrong (misses king safety)
piece.pseudoLegal + filter  correct, interview-sized
bitboards + magic attacks   engine, not 45 minutes

We will use the third. The first is what you write on the board so you can cross it out.

Each piece knows how it moves, not whether the game is still legal after it does.

A pseudo-legal move is geometry plus “do not capture your own piece,” including the specials that piece owns (pawn double, pawn capture, castle attempt). It does not ask “is my king safe?”

A legal move is a pseudo-legal move that, when applied and reverted, leaves Alice’s king unattacked.

interface Piece
  color()
  type()
  pseudoLegalMoves(board, from) -> [Move]
Knight.pseudoLegalMoves:
  for each of 8 L-shaped deltas:
    sq = from + delta
    if onBoard(sq) and board.at(sq) is empty or Bob:
      yield Move(from, sq)

Sliding pieces (bishop, rook, queen) walk a ray until they hit an edge, a friend (stop), or an enemy (capture and stop). The king is one step in eight directions, plus castle attempts if rights and path-empty say so. Pawns are the messy ones — they belong in Pawn, not in Game.

Game owns the filter:

legalMoves(color):
  out = []
  for each (square, piece) of color:
    for m in piece.pseudoLegalMoves(board, square):
      board.apply(m)
      safe = not board.isAttacked(kingOf(color), opponent(color))
      board.unapply(m)
      if safe: out.append(m)
  return out

move(m):
  if m not in legalMoves(sideToMove): throw ILLEGAL_MOVE
  board.apply(m)
  log.append(m)
  recomputeStatus()

apply / unapply must restore castle rights, the en passant square, and the captured piece. If unapply is sloppy, a pin test corrupts the board and every later move is junk.

Why not teach every piece about check? Because a pinned bishop still attacks through the pin for the purpose of Bob’s king. Attack generation and legal-move generation are different questions. The filter keeps them apart.

Alice king e1, Alice bishop e2, Bob rook e8

Bishop on e2 is pinned. Its pseudo-legal list includes e3, e4, …
Filter throws those away: after the bishop leaves e, e1 is checked.
The bishop still attacks e3 for “does Bob’s king step onto e3?”

That last line is the Senior distinction. Pins fall out of the filter. You do not need a pin table.

9. Board representation

An 8×8 array of Piece | empty is the interview board. Files a–h left to right, ranks 1–8 bottom to top when White sits at the bottom.

    a b c d e f g h
  8 r n b q k b n r
  7 p p p p p p p p
  6 . . . . . . . .
  5 . . . . . . . .
  4 . . . . . . . .
  3 . . . . . . . .
  2 P P P P P P P P
  1 R N B Q K B N R

White = uppercase. Black = lowercase.
Alice (White) on ranks 1–2. Bob (Black) on ranks 7–8.

Index with board[rank][file] or board[rank * 8 + file]. Either is fine. Bitboards (one 64-bit mask per piece type) are how engines generate attacks. They are the wrong first drawing: you cannot see Alice’s knight.

Side state that is not in the grid:

toMove              WHITE | BLACK
castleRights        White K, White Q, Black K, Black Q
enPassant           square or none
halfmoveClock       optional (50-move)
fullmoveNumber      optional (PGN)

After e2-e4, enPassant becomes e3 and clears on the next completed turn if unused. After Alice’s king moves, both of her castle flags die. After her h-rook moves or is captured, only White K dies.

Square     file, rank
  a1 = (0,0)    h1 = (7,0)    e4 = (4,3)    e8 = (4,7)

onBoard    0 ≤ file < 8 and 0 ≤ rank < 8

Board.at and Board.place are the only ways to touch cells. Tests build positions with place, not by poking the array.

10. Specials: double, en passant, castle, promotion

Ordinary geometry is not enough. Four rules change extra state. Put them on the piece that owns them, then run the same king-safety filter.

Pawn double-step

From rank 2 (Alice) or rank 7 (Bob), a pawn may step two squares if both the next square and the landing square are empty. It never jumps a piece. The move sets enPassant to the skipped square.

Alice pawn e2, e3 empty, e4 empty
  e2-e4   legal
  enPassant = e3

Alice pawn e2, Bob knight on e3
  e2-e4   illegal (path blocked)

En passant

If Bob just double-stepped a pawn onto a file next to Alice’s pawn, she may capture as if he had stopped on the skipped square. The captured pawn is not on to. apply must remove it. unapply must put it back.

    a b c d e f g h
  5 . . . . P p . .     Alice pawn e5, Bob just played f7-f5
  4 . . . . . . . .

enPassant = f6
Alice  exf6  e.p.   →  pawn lands f6, Bob’s pawn on f5 disappears

Only on the immediate next turn. If Alice plays something else, enPassant clears and the capture dies. The filter still applies: if taking en passant opens her king to a rook on the 5th rank, the move is illegal. That is a real FIDE case. Your apply/unapply has to be good enough to see it.

Castling

Alice may castle if all of these hold:

  1. Rights. Her king and that rook have never moved (flags, not “they sit on home squares”).
  2. Path empty. Squares between king and rook are empty.
  3. Not in check. e1 is not attacked.
  4. Not through check. The square the king crosses is not attacked (f1 for O-O, d1 for O-O-O).
  5. Not into check. The landing square is not attacked (g1 or c1).

O-O moves the king e1-g1 and the rook h1-f1. O-O-O moves the king e1-c1 and the rook a1-d1. One Move with a CASTLE flag; apply moves both pieces.

    a b c d e f g h
  8 . . . . . . . k
  7 p p p p p p p p
  6 b . . . . . . .
  5 . . . . . . . .
  4 . . . . . . . .
  3 . . . . . . . .
  2 P P P P P P P P
  1 R . . . K . . R

Alice wants O-O. Rights yes. f1 and g1 empty.
Bob’s bishop on a6 attacks f1 (a6–b5–c4–d3–e2–f1).
Castle through check. Illegal.

pseudoLegalMoves on the king can yield the castle if (1) and (2) hold. The filter — and an explicit “e1 / f1 / g1 attacked?” check — kill (3)(4)(5). I would test the through-check case even if the generic filter would also reject landing on a checked g1; f1 is the square people forget.

Rights update on apply:

king moves     → that color loses both rights
rook moves     → that color loses that side
rook captured  → opponent loses that side

Sitting on e1 and h1 after a king trip to f1 and back does not restore rights. The flag is history, not a glance at the grid.

Promotion

When Alice’s pawn reaches rank 8 (Bob, rank 1), it must become a queen, rook, bishop, or knight of her color. There is no “stay a pawn.”

move(e7, e8, promotion=QUEEN)
move(e7, d8, promotion=KNIGHT)   // capture + promote

Move.promotion is required on that rank. Defaulting to queen is a UI choice. The object should not silently pick. After promotion, the filter runs as usual: a new queen that does not cover her king is still illegal.

Do not implement these as four extra switch cases in Game. Pawn emits double, capture, en passant, and promotion candidates. King emits castle candidates. Game only filters and applies.

11. Check, checkmate, stalemate

Three questions, in this order.

Is the square attacked? Walk Bob’s pieces and ask whether any of them attacks that square. Pawns attack diagonally, they do not attack the square in front. Knights jump. Sliders need a clear ray. Do not call legalMoves here — you will recurse into “is the king safe?” forever.

isAttacked(square, byColor):
  for each (from, piece) of byColor:
    if square in piece.attacks(board, from):
      return true
  return false

inCheck(color):
  return isAttacked(kingSquare(color), opponent(color))

attacks is almost pseudoLegalMoves with two edits: pawns use capture squares only, and you may ignore “would this leave my king safe?” A pinned rook still checks Alice if it sees e1.

Checkmate: Alice is in check and legalMoves(WHITE) is empty. Every attempt to step the king, block, or capture fails the filter.

Stalemate: Alice is not in check and legalMoves(WHITE) is empty. The game is a draw. Forgetting the “not in check” clause turns a stalemate into a fake mate.

after Alice’s legal move:
  if Bob in check:
    if Bob has no legal move:  CHECKMATE   (Alice wins)
    else:                      CHECK
  else:
    if Bob has no legal move:  STALEMATE   (draw)
    else:                      IN_PROGRESS

Check is not a third kind of move. It is a property of the position Bob is about to play from. A UI can flash the king. move() still goes through the same filter.

Check      king attacked, at least one legal reply
Checkmate  king attacked, zero legal replies
Stalemate  king safe,     zero legal replies

Double check is free: two attackers, and the filter will usually leave only king moves. You do not need a special case.

12. State machine for Game

One machine. Do not encode “White to move and in check” as twelve enums.

  READY
    │ start()
  IN_PROGRESS ──────► CHECK ──────► IN_PROGRESS
    │                  │
    │                  │  no legal reply
    │                  ▼
    │               CHECKMATE
    │  no legal reply, not in check
  STALEMATE

  IN_PROGRESS / CHECK ── resign(color) ──► RESIGNED

READY is the constructed object before start(), or the standard position with no move yet — pick one and stick to it. I treat start() as “clocks may start; White may move,” and the first move stays in IN_PROGRESS unless it happens to check.

Terminal states: CHECKMATE, STALEMATE, RESIGNED. move is illegal there. resign in a terminal state is a no-op or GAME_OVER — say which. I would no-op and return the same status.

IN_PROGRESS / CHECK  →  move(legal)     →  recompute
IN_PROGRESS / CHECK  →  move(illegal)   →  unchanged (throw)
IN_PROGRESS / CHECK  →  resign          →  RESIGNED
CHECKMATE / STALEMATE / RESIGNED         →  no board writes

Illegal: CHECKMATE → IN_PROGRESS, accepting a move that leaves the mover in check, castling after the king has already moved. CHECK is a decorating state on an otherwise live game. Some teams keep only IN_PROGRESS plus inCheck. The user-facing machine above is easier to talk through. Internally, inCheck plus “legal list empty” is enough to derive it.

Draw by agreement is a handshake around this machine (DRAW), not a new move type. 50-move and threefold read the log; they are extra terminals you name if asked.

13. Move log, undo, PGN

apply mutates. The log is how you remember what to undo.

Move
  from, to
  piece
  captured?          // piece + square (e.p. square ≠ to)
  promotion?
  flags              // QUIET | CAPTURE | DOUBLE | EN_PASSANT | CASTLE
  prevCastleRights
  prevEnPassant

Undo is unapply(log.pop()). You restore the grid and the two side fields. A log that stores only from/to cannot undo en passant or a lost castle right.

PGN is a serialization of that log (1. e4 e5 2. Nf3 ...). Mention it. Do not write a PGN parser in this interview. FEN snapshots the current position; PGN snapshots the story. The object graph is still the source of truth.

If they want takeback: undo() is legal in IN_PROGRESS and CHECK, not after mate, unless you are debugging a test. Two undos after 1. e4 e5 return the start. The filter does not change.

14. Class sketch

Ideas, not a framework dump. One Game. Two players call it in turn.

Color          WHITE, BLACK
PieceType      KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN
Square         file, rank
  plus(df, dr) -> Square | off
  algebraic()  -> "e4"

Piece          color, type
  Knight, Bishop, Rook, Queen, King, Pawn
  pseudoLegalMoves(board, from) -> [Move]
  attacks(board, from) -> [Square]

Move           from, to, piece, captured?, promotion?, flags
               prevCastleRights, prevEnPassant

Board
  at(square) -> Piece?
  apply(move) / unapply(move)
  kingSquare(color) -> Square
  isAttacked(square, by) -> bool
  inCheck(color) -> bool
  castleRights, enPassant, toMove

Game
  start()
  move(from, to, promotion?) -> MoveResult
  legalMoves() -> [Move]
  resign(color)
  status() -> READY | IN_PROGRESS | CHECK
              | CHECKMATE | STALEMATE | RESIGNED
  undo()                    // optional

Game.move in one breath:

move(from, to, promotion?):
  if status is terminal: throw GAME_OVER
  piece = board.at(from)
  if piece is empty or piece.color != board.toMove:
    throw NOT_YOUR_TURN

  candidate = match among piece.pseudoLegalMoves(...)
  if no candidate: throw ILLEGAL_MOVE
  if pawn-to-last-rank and promotion missing: throw PROMOTION_REQUIRED
  candidate.promotion = promotion

  board.apply(candidate)
  if board.inCheck(mover):
    board.unapply(candidate)
    throw ILLEGAL_MOVE          // left own king in check

  log.append(candidate)
  board.toMove = opponent
  status = derive(board)        // check / mate / stale
  return MoveResult(candidate, status)

Handlers do not poke board[rank][file]. Tests build a Board, hang two Piece instances, and call legalMoves. They do not subclass Game to stub a knight.

Factory: Piece.of(type, color) returns the strategy. Adding a fairy piece later is a new class, not a new case in move.

15. Tests that matter

Skip getters. Test the invariant: the side that just moved is never in check, and the opponent’s result is mate, stale, or a live position.

  1. Leave-in-check. Position from section 7. move(b1, c3)ILLEGAL_MOVE. Board unchanged. Status still Alice to move.
  2. Castle through check. Bishop on a6, Alice O-O → rejected. Rights still present. King still on e1.
  3. Castle out of check. Rook on e8, Alice O-O → rejected even if f1 and g1 are empty and quiet.
  4. Pawn promotion. Pawn e7, move(e7, e8, QUEEN) → e8 is a White queen, e7 empty. Missing promotion → PROMOTION_REQUIRED.
  5. En passant. Bob just played f7-f5. Alice e5xf6 e.p. → pawn on f6, f5 empty, enPassant cleared.
  6. En passant expired. After f7-f5, Alice plays some other legal move. Bob can no longer be taken e.p. on f6.
  7. Checkmate. A known mate (back-rank, or queen on the hole). legalMoves empty, inCheck true, status CHECKMATE. Further moveGAME_OVER.
  8. Stalemate. King boxed, not attacked, zero legal moves. Status STALEMATE, not CHECKMATE.
  9. Pin. Bishop on the king’s file cannot step off it. Capturing the checking rook on that file is legal if it exists.
  10. Wrong turn / resign. Bob moves on White’s turn → NOT_YOUR_TURN. Alice resigns in CHECKRESIGNED. No more moves.

Tests (1) and (2) are the offer. If they only have time for two, run leave-in-check and castle-through-check.

16. What you will not write in 45 minutes

Say this out loud so they stop steering you into an engine.

Not an evaluator. No material tables, no piece-square scores, no “who is winning.” Mate and stale are rule outcomes, not search.

Not a search. Minimax, alpha-beta, quiescence, iterative deepening — that is how Stockfish chooses a move. This interview is whether Alice may make one.

Not bitboards. Pretty, fast, and a whiteboard tar pit. 8×8 plus rays is the Senior-sized generator.

Not a network product. No rooms, no WebSocket move stream, no Elo, no anti-cheat. If they want “online chess,” the room still calls this Game.move. The hard object does not change.

Not every FIDE draw. 50-move and threefold need a clock and a position hash. Name them. Implement them only if they ask and you have ten spare minutes.

Not a clock inside move. A Clock wrapping Game can flag Alice on timeout and jump to RESIGNED (or FLAG). Mixing seconds into isLegal is how people fail the castle tests.

Snake and Ladder is the same cut: rules and state, not a real-time platform. Parking lot is the same cut with a race instead of a filter.

17. Final architecture / object diagram

One game. Two callers. One filter.

          Alice (White)                         Bob (Black)
              |                                      |
              |  move(from, to, promo?) / resign     |
              v                                      v
        +----------------------------------------------+
        |                     Game                     |
        |  start / move / legalMoves / resign / undo   |
        |  status, move log                            |
        +-------------------+--------------------------+
                            |
                            v
                    +---------------+
                    |     Board     |
                    |  8×8 cells    |
                    |  toMove       |
                    |  castleRights |
                    |  enPassant    |
                    |  apply/unapply|
                    |  isAttacked   |
                    +-------+-------+
                            |
          +-----------------+------------------+
          |                 |                  |
          v                 v                  v
       Piece             Move               Square
       Knight            from, to           file, rank
       Bishop            flags
       Rook              captured
       Queen             promotion
       King              prev rights
       Pawn
move  your turn? -> match pseudo-legal -> apply -> own king safe?
      no  -> unapply, ILLEGAL
      yes -> log, flip side, derive CHECK / MATE / STALE

Kafka, Redis, and a region pair are not in this picture. If those words appear, you left LLD.

Source of truth    Board cells + castle rights + en passant + toMove
Derived            status, in-check highlight, legal-move dots
Ephemeral          UI arrows, optional clock display
Audit              move log (undo / PGN)

18. Interview-ready summary

How to walk through in 10–15 minutes

0–2 min. Alice’s knight, Bob’s rook on the e-file. LLD, not an engine.
2–4 min. Standard chess, two players, no variants. Correctness over speed.
4–6 min. Entities: Square, Piece, Move, Board, Game. Rights and e.p. are state.
6–9 min. Naive switch in Game. Piece strategies emit pseudo-legal. Filter = own king safe.
9–12 min. Specials: double, e.p., castle (rights + path + not in/through/into), promotion.
12–15 min. Check / mate / stale, the state machine, two tests, what you will not write.

Key decisions to remember

  1. This is objects and a legality filter, not Stockfish and not a lobby.
  2. Pseudo-legal is geometry; legal is “after apply, my king is not attacked.”
  3. Pins, discovered check, and “must move out of check” fall out of that filter.
  4. isAttacked must not call legalMoves — attacks ≠ legal moves (pawns, pins).
  5. Castle rights are history flags, not “pieces look at home.”
  6. Castling needs empty path and not in / through / into check.
  7. En passant captures a pawn that is not on to; unapply has to know that.
  8. Promotion is part of the move, not a second API after the fact.
  9. Mate = in check + no legal move. Stale = not in check + no legal move.
  10. One Game per match. Ten thousand rooms is ten thousand of these, not Kafka.

Likely interviewer follow-up questions

  • How do you know a move leaves the king in check?
  • Why not put check logic on each piece?
  • How do you castle? What if f1 is attacked?
  • When is en passant legal, and what does apply delete?
  • Pawn to the 8th with no promotion piece?
  • Difference between checkmate and stalemate?
  • How would you undo? What must a Move store?
  • How would you add a clock without breaking tests?
  • What if they ask for an AI opponent?
  • What if they ask for online multiplayer?

Senior-level points that differentiate the answer

  • Name the illegal-in-check bug before drawing classes. Classes without a filter are a vocabulary list.
  • Separate attacks from legal moves. That is how you avoid recursion and how pins still check.
  • Treat castle rights and en passant as first-class state, not something you infer from the grid.
  • Call out castle-through-check (f1) without being prompted.
  • apply / unapply as the test seam; do not clone the whole board unless you say why (cloning is simpler and slower — fine here).
  • Refuse the engine and the WebSocket product in one sentence, then finish the rules.

A 1–2 minute verbal answer

Chess LLD is a board, pieces that generate pseudo-legal moves, and a game that only accepts a move if the mover’s king is safe afterward. I would not put a giant switch in Game — promotion, en passant, and castling explode it. Each piece type owns its geometry. Sliding pieces walk rays; knights jump; pawns own the double-step, captures, en passant, and promotion; the king owns one-step moves and castle attempts. Castling also needs rights, an empty path, and no check on the from, through, or to squares. After I apply a candidate, I ask whether that side’s king is attacked. If yes, I unapply and reject. Check is “king square attacked.” Checkmate is check plus no legal reply. Stalemate is no check plus no legal reply. I would test leaving the king in check and castling through check. I would not write an evaluator, a search, or a multiplayer stack in this interview. A chess server is many of these Game objects behind a room id.

For HLD framing around this, see the System Design Interview Complete Guide and the questions hub.