- Chess is the LLD problem where an inheritance hierarchy is justified: pieces differ in behaviour. Factor shared logic into
SlidingPiece(rook, bishop, queen) andStepPiece(knight, king). - Validate in two layers: pieces generate pseudo-legal moves; the game filters out moves that leave your king in check by make, test, unmake.
- Checkmate = no legal moves and in check. Stalemate = no legal moves and not in check. Do not special-case escapes.
- Each
Moveis a Command withexecute/undo, storing captured piece, capture square (en passant), promotion and rook squares (castling). - The Java below handles castling, en passant and promotion and reproduces the published perft counts (20, 400, 8,902 from the start; 48 and 2,039 for the "Kiwipete" test position), verified with a line-by-line port.
Most LLD problems punish you for inheritance. Vehicles in a parking lot do not need subclasses; snakes and ladders are the same thing pointing in different directions. Chess is the exception, and that is precisely why interviewers use it: they want to see whether you reach for a hierarchy because it fits or because it is a reflex, and whether your hierarchy survives the hard parts: pins, castling through check, en passant, and undo.
This guide builds a complete, correct two-player chess model. It goes further than the usual "Piece has canMove()" answer, because that answer breaks the moment the interviewer asks "what if moving this bishop exposes your king?" If you need a refresher on the OOP fundamentals behind it, the OOP interview questions guide covers polymorphism and abstraction with examples.
Scope and Requirements for Chess LLD
In scope
- Two players alternate moves on a standard 8×8 board from the standard starting position.
- Only legal moves are accepted, including castling, en passant and promotion (with under-promotion).
- The game reports check, checkmate and stalemate.
- Moves can be undone (and, as an extension, redone).
- Moves are entered in coordinate notation such as
e2e4ore7e8n.
Out of scope (say so explicitly)
- A chess engine (AI). Mention it as a
Playerstrategy extension. - Clocks, online play, rating. These are system-design concerns.
- Draw by threefold repetition, the 50-move rule and insufficient material. Easy to add; covered in extensions.
Scoping like this in the first five minutes matters. Full chess rules are big, and an interviewer would rather see a correct core with clear extension points than a sprawling half-finished rulebook.
Chess Domain Model
| Class | Responsibility | Key decision |
|---|---|---|
Position | Immutable square (file, rank), parsing "e4" | Record with validation; offset() returns null off-board |
Color | WHITE, BLACK, opposite() | Enum |
Piece hierarchy | Squares attacked; pseudo-legal moves | Abstract class; pieces do not store their position |
Board | Grid of pieces, isAttacked, findKing | Single source of truth for where pieces are |
Move | Command: execute and undo itself | Stores everything needed to reverse |
Game | Turn, history, legal move filter, status | The only class that knows the rules of turns |
The Piece Hierarchy: When Inheritance Is Right
Why a hierarchy here and not in the parking lot? Because the subclasses differ in behaviour, not just in a label. A rook walks orthogonally until blocked; a knight jumps to eight fixed offsets; a pawn moves one way, captures another way, and transforms on the last rank. Polymorphism lets Game ask every piece the same question without a switch on type.
The two intermediate classes remove duplication. Rook, bishop and queen are the same algorithm with different direction tables, so SlidingPiece implements the walk once and each subclass supplies directions. Knight and king are "check a fixed list of offsets", so StepPiece does that once. This is the Template Method idea: the base class owns the algorithm, subclasses fill in data.
Inheritance vs composition for movement
A popular alternative is composition: one Piece class holding a list of MovementStrategy objects (slide orthogonal, slide diagonal, knight jump). The queen gets two sliding strategies. This is more flexible for fairy-chess variants ("a piece that moves like a knight or a rook"). For standard chess, the hierarchy is simpler to read. Mention both; pick one; explain why. The SOLID principles guide discusses when to favour composition.
Why separate attackedSquares from pseudoMoves?
Two reasons that usually surface only when someone has actually written a chess program:
- Pawns attack differently than they move. A pawn on e4 attacks d5 and f5 even if they are empty (which matters for "can the king step there?"), but it can only move to e5.
- Castling would recurse. Castling requires checking that the king does not pass through attacked squares. If "attacked" were computed from pseudo-moves, the opponent king's castling generation would call back into attack detection, potentially forever. Attacks never include castling, so there is no recursion.
Move Validation: Pseudo-Legal, Then Legal
Move validation is the heart of the interview. Weak answers put a isValidMove(from, to) on each piece and stop. That misses the rule that dominates real games: you may not make any move that leaves your own king in check. That rule covers pins, moving the king into an attacked square, ignoring a check, and the notorious en passant discovered check along a rank.
- Geometry (piece level). Generate target squares from movement rules: directions, offsets, pawn pushes.
- Occupancy (piece level). Stop sliding at the first occupied square; allow it only if it holds an enemy (capture).
- Special-move preconditions (piece level). Castling rights and empty/unattacked squares; en passant only immediately after the enemy pawn's double step; promotion on the last rank.
- King safety (game level). For each pseudo-legal move: execute it, check whether your king's square is attacked, undo it. Keep it only if safe.
- Turn (game level). Only pieces of the side to move are considered.
The make-test-unmake approach is how real move generators handle legality in the simple case (strong engines optimise it with pin detection, but the semantics are identical). It is short, it is obviously correct, and it reuses the Command pattern you need for undo anyway. That reuse is a strong point to make out loud.
Detecting Check, Checkmate and Stalemate
| Status | King attacked? | Legal moves exist? |
|---|---|---|
| Normal play | No | Yes |
| Check | Yes | Yes |
| Checkmate | Yes | No |
| Stalemate (draw) | No | No |
That table is the entire algorithm. Candidates who try to detect checkmate directly ("can the king move? can something capture the attacker? can something block?") almost always miss a case: double check (only king moves help), a blocker that is itself pinned, or capturing the checking pawn en passant. Generating legal moves and counting them handles all of it, because the legality filter already encodes every rule.
Is the king attacked? Scan the opponent's pieces and ask whether any of them has the king's square in attackedSquares. That is O(pieces × reach), at most a few hundred square checks, which is negligible for a game UI. Engines invert it (look outward from the king along rays and knight jumps), which is a good optimisation to mention.
Special Moves: Castling, En Passant, Promotion
Castling: five conditions
- The king has never moved.
- The chosen rook has never moved.
- All squares between them are empty.
- The king is not currently in check.
- The king does not pass through or land on an attacked square (for queen-side, the b-file square must be empty but may be attacked).
"Has never moved" is tracked by a moveCount on each piece, incremented by execute and decremented by undo. A boolean hasMoved is a classic bug: after undoing the king's first move you cannot know whether to reset it to false. A counter restores exactly.
En passant
Legal only on the move immediately after an enemy pawn advances two squares and lands beside your pawn. The capture lands on the square the enemy pawn skipped, but removes the pawn from a different square. That is why Move stores capturedAt separately from to. The "immediately after" rule means the move generator needs the last move, which the game's history stack provides.
Promotion
A pawn reaching the last rank becomes a queen, rook, bishop or knight. Generate all four as separate moves (under-promotion to a knight sometimes delivers mate). The Move stores the new piece; undo puts the original pawn back.
Command Pattern for Undo (and Redo)
Each Move implements Command: execute(board) and undo(board). The game keeps a stack of executed moves. Undo pops and reverses; redo (an extension) pushes onto a second stack that is cleared when a new move is played, the same model every text editor uses.
What a move must remember
| Field | Why undo needs it |
|---|---|
from, to, piece | Move the piece back |
captured | Restore the captured piece |
capturedAt | En passant captures from a square other than to |
promotion | Replace the new piece with the original pawn |
rookFrom, rookTo | Castling moves two pieces |
piece moveCount (on the piece) | Restores castling rights and pawn double-step eligibility |
Command vs Memento
The alternative is Memento: snapshot the whole board before every move and restore it on undo. It is simpler to get right and costs 64 references per move, which is trivial for a human game. Commands are cheaper per move, double as the move log for PGN export, and are needed anyway for make-unmake validation, so they win here. Saying "Memento would also work, here is why I chose Command" is the kind of trade-off reasoning the round rewards. For a broader catalogue of these patterns, see top 10 design patterns for interviews.
Full Java Code for the Chess Game
Java 17+, single file, run with java ChessDemo.java. Coordinates are zero-based: file 0 = a, rank 0 = 1.
import java.util.*;
public class ChessDemo {
enum Color {
WHITE, BLACK;
Color opposite() { return this == WHITE ? BLACK : WHITE; }
}
enum Status { ACTIVE, CHECK, CHECKMATE, STALEMATE }
record Position(int file, int rank) {
Position {
if (!inside(file, rank)) throw new IllegalArgumentException("Off board: " + file + "," + rank);
}
static boolean inside(int f, int r) { return f >= 0 && f < 8 && r >= 0 && r < 8; }
Position offset(int df, int dr) {
return inside(file + df, rank + dr) ? new Position(file + df, rank + dr) : null;
}
static Position parse(String s) { return new Position(s.charAt(0) - 'a', s.charAt(1) - '1'); }
@Override public String toString() { return "" + (char) ('a' + file) + (rank + 1); }
}
// ===================== Moves as Commands =====================
interface Command {
void execute(Board b);
void undo(Board b);
}
static final class Move implements Command {
final Position from, to;
final Piece piece;
final Piece captured; // null if none
final Position capturedAt; // differs from 'to' only for en passant
final Piece promotion; // null unless promoting
final Position rookFrom, rookTo; // castling only
private Move(Position from, Position to, Piece piece, Piece captured, Position capturedAt,
Piece promotion, Position rookFrom, Position rookTo) {
this.from = from; this.to = to; this.piece = piece;
this.captured = captured; this.capturedAt = capturedAt;
this.promotion = promotion; this.rookFrom = rookFrom; this.rookTo = rookTo;
}
static Move quiet(Position f, Position t, Piece p) { return new Move(f, t, p, null, null, null, null, null); }
static Move capture(Position f, Position t, Piece p, Piece c) { return new Move(f, t, p, c, t, null, null, null); }
static Move promotion(Position f, Position t, Piece p, Piece c, Piece promo) {
return new Move(f, t, p, c, c == null ? null : t, promo, null, null);
}
static Move enPassant(Position f, Position t, Piece p, Piece c, Position cAt) {
return new Move(f, t, p, c, cAt, null, null, null);
}
static Move castle(Position f, Position t, Piece king, Position rf, Position rt) {
return new Move(f, t, king, null, null, null, rf, rt);
}
public void execute(Board b) {
if (captured != null) b.set(capturedAt, null);
b.set(from, null);
b.set(to, promotion != null ? promotion : piece);
piece.moveCount++;
if (rookFrom != null) {
Piece rook = b.get(rookFrom);
b.set(rookFrom, null);
b.set(rookTo, rook);
rook.moveCount++;
}
}
public void undo(Board b) {
if (rookFrom != null) {
Piece rook = b.get(rookTo);
b.set(rookTo, null);
b.set(rookFrom, rook);
rook.moveCount--;
}
piece.moveCount--;
b.set(to, null);
b.set(from, piece);
if (captured != null) b.set(capturedAt, captured);
}
@Override public String toString() {
return "" + from + to + (promotion == null ? "" : String.valueOf(Character.toLowerCase(promotion.letter())));
}
}
// ===================== Pieces =====================
static final int[][] ORTHOGONAL = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static final int[][] DIAGONAL = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
static final int[][] ALL_DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
static final int[][] KNIGHT_JUMPS = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}};
abstract static class Piece {
final Color color;
int moveCount; // castling rights; restored exactly on undo
Piece(Color color) { this.color = color; }
abstract char letter();
char symbol() { return color == Color.WHITE ? letter() : Character.toLowerCase(letter()); }
/** Squares this piece controls (used for check detection). Never includes castling. */
abstract List<Position> attackedSquares(Board b, Position from);
/** Moves by movement rules only; may leave own king in check (Game filters those). */
List<Move> pseudoMoves(Board b, Position from, Move lastMove) {
List<Move> out = new ArrayList<>();
for (Position to : attackedSquares(b, from)) {
Piece target = b.get(to);
if (target == null) out.add(Move.quiet(from, to, this));
else if (target.color != color) out.add(Move.capture(from, to, this, target));
}
return out;
}
}
abstract static class SlidingPiece extends Piece {
SlidingPiece(Color c) { super(c); }
abstract int[][] directions();
List<Position> attackedSquares(Board b, Position from) {
List<Position> out = new ArrayList<>();
for (int[] d : directions()) {
Position p = from.offset(d[0], d[1]);
while (p != null) {
out.add(p);
if (b.get(p) != null) break; // blocked beyond this square
p = p.offset(d[0], d[1]);
}
}
return out;
}
}
abstract static class StepPiece extends Piece {
StepPiece(Color c) { super(c); }
abstract int[][] offsets();
List<Position> attackedSquares(Board b, Position from) {
List<Position> out = new ArrayList<>();
for (int[] o : offsets()) {
Position p = from.offset(o[0], o[1]);
if (p != null) out.add(p);
}
return out;
}
}
static final class Rook extends SlidingPiece {
Rook(Color c) { super(c); }
char letter() { return 'R'; }
int[][] directions() { return ORTHOGONAL; }
}
static final class Bishop extends SlidingPiece {
Bishop(Color c) { super(c); }
char letter() { return 'B'; }
int[][] directions() { return DIAGONAL; }
}
static final class Queen extends SlidingPiece {
Queen(Color c) { super(c); }
char letter() { return 'Q'; }
int[][] directions() { return ALL_DIRECTIONS; }
}
static final class Knight extends StepPiece {
Knight(Color c) { super(c); }
char letter() { return 'N'; }
int[][] offsets() { return KNIGHT_JUMPS; }
}
static final class King extends StepPiece {
King(Color c) { super(c); }
char letter() { return 'K'; }
int[][] offsets() { return ALL_DIRECTIONS; }
@Override
List<Move> pseudoMoves(Board b, Position from, Move lastMove) {
List<Move> out = super.pseudoMoves(b, from, lastMove);
if (moveCount == 0 && !b.isAttacked(from, color.opposite())) {
tryCastle(b, from, out, 7, 5, 6); // king side: rook h->f, king e->g
tryCastle(b, from, out, 0, 3, 2); // queen side: rook a->d, king e->c
}
return out;
}
private void tryCastle(Board b, Position from, List<Move> out, int rookFile, int rookToFile, int kingToFile) {
Position rookPos = new Position(rookFile, from.rank());
Piece rook = b.get(rookPos);
if (!(rook instanceof Rook) || rook.color != color || rook.moveCount != 0) return;
int step = rookFile > from.file() ? 1 : -1;
for (int f = from.file() + step; f != rookFile; f += step) {
if (b.get(new Position(f, from.rank())) != null) return; // path must be empty
}
for (int f = from.file() + step; f != kingToFile + step; f += step) {
if (b.isAttacked(new Position(f, from.rank()), color.opposite())) return; // no passing through check
}
out.add(Move.castle(from, new Position(kingToFile, from.rank()), this,
rookPos, new Position(rookToFile, from.rank())));
}
}
static final class Pawn extends Piece {
Pawn(Color c) { super(c); }
char letter() { return 'P'; }
private int dir() { return color == Color.WHITE ? 1 : -1; }
List<Position> attackedSquares(Board b, Position from) {
List<Position> out = new ArrayList<>();
for (int df : new int[] {-1, 1}) {
Position p = from.offset(df, dir());
if (p != null) out.add(p);
}
return out;
}
@Override
List<Move> pseudoMoves(Board b, Position from, Move lastMove) {
List<Move> out = new ArrayList<>();
int lastRank = color == Color.WHITE ? 7 : 0;
Position one = from.offset(0, dir());
if (one != null && b.get(one) == null) {
addWithPromotion(out, from, one, null, lastRank);
int startRank = color == Color.WHITE ? 1 : 6;
if (from.rank() == startRank) {
Position two = from.offset(0, 2 * dir());
if (b.get(two) == null) out.add(Move.quiet(from, two, this));
}
}
for (Position diag : attackedSquares(b, from)) {
Piece target = b.get(diag);
if (target != null && target.color != color) addWithPromotion(out, from, diag, target, lastRank);
}
// En passant: only right after an adjacent enemy pawn's double step
if (lastMove != null && lastMove.piece instanceof Pawn && lastMove.piece.color != color
&& Math.abs(lastMove.to.rank() - lastMove.from.rank()) == 2
&& lastMove.to.rank() == from.rank()
&& Math.abs(lastMove.to.file() - from.file()) == 1) {
Position target = new Position(lastMove.to.file(), from.rank() + dir());
out.add(Move.enPassant(from, target, this, lastMove.piece, lastMove.to));
}
return out;
}
private void addWithPromotion(List<Move> out, Position from, Position to, Piece captured, int lastRank) {
if (to.rank() == lastRank) {
for (Piece promo : List.of(new Queen(color), new Rook(color), new Bishop(color), new Knight(color))) {
out.add(Move.promotion(from, to, this, captured, promo));
}
} else {
out.add(captured == null ? Move.quiet(from, to, this) : Move.capture(from, to, this, captured));
}
}
}
// ===================== Board =====================
static final class Board {
private final Piece[][] grid = new Piece[8][8]; // [file][rank]
Piece get(Position p) { return grid[p.file()][p.rank()]; }
void set(Position p, Piece piece) { grid[p.file()][p.rank()] = piece; }
static Board standard() {
Board b = new Board();
for (Color c : Color.values()) {
int back = c == Color.WHITE ? 0 : 7;
int pawns = c == Color.WHITE ? 1 : 6;
Piece[] row = {new Rook(c), new Knight(c), new Bishop(c), new Queen(c),
new King(c), new Bishop(c), new Knight(c), new Rook(c)};
for (int f = 0; f < 8; f++) {
b.grid[f][back] = row[f];
b.grid[f][pawns] = new Pawn(c);
}
}
return b;
}
boolean isAttacked(Position target, Color by) {
for (int f = 0; f < 8; f++) {
for (int r = 0; r < 8; r++) {
Piece p = grid[f][r];
if (p != null && p.color == by && p.attackedSquares(this, new Position(f, r)).contains(target)) {
return true;
}
}
}
return false;
}
Position findKing(Color c) {
for (int f = 0; f < 8; f++)
for (int r = 0; r < 8; r++)
if (grid[f][r] instanceof King && grid[f][r].color == c) return new Position(f, r);
throw new IllegalStateException("No " + c + " king");
}
String render() {
StringBuilder sb = new StringBuilder();
for (int r = 7; r >= 0; r--) {
sb.append(r + 1).append(' ');
for (int f = 0; f < 8; f++) sb.append(grid[f][r] == null ? '.' : grid[f][r].symbol()).append(' ');
sb.append('\n');
}
return sb.append(" a b c d e f g h").toString();
}
}
// ===================== Game =====================
static final class Game {
private final Board board;
private Color turn = Color.WHITE;
private final Deque<Move> history = new ArrayDeque<>();
Game(Board board) { this.board = board; }
Board board() { return board; }
Color turn() { return turn; }
List<Move> legalMoves() {
List<Move> legal = new ArrayList<>();
Move last = history.peekLast();
for (int f = 0; f < 8; f++) {
for (int r = 0; r < 8; r++) {
Position from = new Position(f, r);
Piece p = board.get(from);
if (p == null || p.color != turn) continue;
for (Move m : p.pseudoMoves(board, from, last)) {
m.execute(board); // make
boolean unsafe = board.isAttacked(board.findKing(turn), turn.opposite());
m.undo(board); // unmake
if (!unsafe) legal.add(m);
}
}
}
return legal;
}
/** Coordinate notation: "e2e4", promotion "e7e8n" (defaults to queen). */
void play(String uci) {
Position from = Position.parse(uci.substring(0, 2));
Position to = Position.parse(uci.substring(2, 4));
char promo = uci.length() > 4 ? Character.toLowerCase(uci.charAt(4)) : 'q';
for (Move m : legalMoves()) {
if (m.from.equals(from) && m.to.equals(to)
&& (m.promotion == null || Character.toLowerCase(m.promotion.letter()) == promo)) {
apply(m);
return;
}
}
throw new IllegalArgumentException("Illegal move: " + uci);
}
void undo() {
Move m = history.pollLast();
if (m == null) throw new IllegalStateException("Nothing to undo");
m.undo(board);
turn = turn.opposite();
}
Status status() {
boolean inCheck = board.isAttacked(board.findKing(turn), turn.opposite());
boolean hasMoves = !legalMoves().isEmpty();
if (hasMoves) return inCheck ? Status.CHECK : Status.ACTIVE;
return inCheck ? Status.CHECKMATE : Status.STALEMATE;
}
/** Counts leaf nodes of the legal move tree: the standard move-generator test. */
long perft(int depth) {
if (depth == 0) return 1;
long nodes = 0;
for (Move m : legalMoves()) {
apply(m);
nodes += perft(depth - 1);
undo();
}
return nodes;
}
private void apply(Move m) {
m.execute(board);
history.addLast(m);
turn = turn.opposite();
}
}
public static void main(String[] args) {
Game game = new Game(Board.standard());
for (int d = 1; d <= 3; d++) System.out.println("perft(" + d + ") = " + game.perft(d));
// Scholar's mate
for (String mv : "e2e4 e7e5 f1c4 b8c6 d1h5 g8f6 h5f7".split(" ")) game.play(mv);
System.out.println(game.board().render());
System.out.println("Status: " + game.status());
game.undo();
System.out.println("After undo: " + game.status() + ", " + game.turn() + " to move");
// En passant
Game ep = new Game(Board.standard());
for (String mv : "e2e4 a7a6 e4e5 d7d5 e5d6".split(" ")) ep.play(mv);
System.out.println(ep.board().render());
}
}
Expected output
perft(1) = 20
perft(2) = 400
perft(3) = 8902
8 r . b q k b . r
7 p p p p . Q p p
6 . . n . . n . .
5 . . . . p . . .
4 . . B . P . . .
3 . . . . . . . .
2 P P P P . P P P
1 R N B . K . N R
a b c d e f g h
Status: CHECKMATE
After undo: ACTIVE, WHITE to move
8 r n b q k b n r
7 . p p . p p p p
6 p . . P . . . .
5 . . . . . . . .
4 . . . . . . . .
3 . . . . . . . .
2 P P P P . P P P
1 R N B Q K B N R
a b c d e f g h
In the scholar's mate, the queen on f7 checks the king on e8. The king cannot take it (the bishop on c4 defends f7), e7 is covered by the queen, d8 and f8 are occupied by Black's own pieces, and nothing can capture or block an adjacent checker. The legality filter finds zero moves while the king is attacked, so the status is checkmate with no special-case code. In the second board, White's e5 pawn captured d5 en passant: the pawn now stands on d6 and the black d-pawn is gone.
Testing a Chess Move Generator with Perft
How do you know your move generator is right? Not by playing a few games. The standard technique is perft (performance test): count every leaf of the legal move tree to depth N and compare with published numbers, such as those on the Chess Programming Wiki perft page.
| Position | Depth | Expected nodes | What it exercises |
|---|---|---|---|
| Start position | 1 | 20 | Pawn single/double pushes, knight jumps |
| Start position | 2 | 400 | Both sides' replies |
| Start position | 3 | 8,902 | First captures and checks |
| "Kiwipete" test position | 1 | 48 | Both castles, pins, many captures |
| "Kiwipete" test position | 2 | 2,039 | Castling through attacked squares, promotions nearby |
The design in this article was checked against all five numbers using a line-by-line port of the same classes and logic. Perft is also a great interview talking point: it shows you think about verification, not just implementation. If the counts disagree, "divide" (print per-first-move counts) and compare with a reference engine to find the exact buggy move type.
Complexity
For a human-vs-human game this is instant. For an engine searching millions of positions per second you would switch to bitboards (one 64-bit integer per piece type), incremental attack tables and pin detection instead of make-unmake for every move. Mentioning that boundary shows you know where the simple design stops.
Extensions Interviewers Ask
Players: human, AI, remote
Introduce interface Player { Move chooseMove(Game game); }. HumanPlayer reads input, RandomPlayer picks any legal move, MinimaxPlayer searches using legalMoves, play and undo (the Command pattern makes search trivial). A remote player wraps a network connection. The game loop does not change. This is Strategy.
Draw rules
- Fifty-move rule: a half-move counter reset on pawn moves and captures; store the previous value in the
Moveso undo restores it. - Threefold repetition: hash each position (Zobrist hashing: XOR of random 64-bit keys per piece-square, side to move, castling rights and en passant file) and count occurrences in a map; decrement on undo.
- Insufficient material: king vs king, king and bishop vs king, king and knight vs king, and same-coloured bishops.
FEN and PGN
FEN describes a position in one line (useful for loading puzzles and tests); a BoardFactory.fromFen is a natural Factory. PGN records a game in standard algebraic notation; generating it from the move history needs disambiguation ("Nbd2" vs "Nfd2"), a nice small exercise.
Online multiplayer and clocks
The game model stays the same; you add a server that owns Game instances, validates every incoming move with play(), and pushes updates to both clients. Real-time push options are compared in WebSockets vs SSE vs long polling. A chess clock is a per-player remaining-time counter with increment, updated on each move server-side.
Observer for UI and logs
A board view, a captured-pieces panel and a move list subscribe to "move played" and "move undone" events. The domain stays free of UI code.
How to Present Chess in an LLD Interview
- Scope (3–5 min). Standard rules, two local players, undo. Declare engine, clocks and draw rules out of scope.
- Model (5–10 min). Position, Color, Piece hierarchy with the two intermediate classes, Board, Move, Game. State that pieces do not store position.
- Validation story (5 min). Pseudo-legal at piece level, legal via make-test-unmake at game level; checkmate and stalemate from the legal move count.
- Code (20 min). Sliding and step pieces first, then pawn, then king safety filter. Leave castling and en passant as named methods if time is short and describe them.
- Undo and testing (5 min). Command fields, moveCount counters, perft as verification.
Mistakes that sink chess LLD answers
canMove(from, to)per piece with no king-safety check. Pins and moving into check go unhandled.- A giant
switch (pieceType)in the board class, defeating the hierarchy. - Storing position in both piece and board.
- A
hasMovedboolean that cannot be undone correctly. - Detecting checkmate by hand-coded escape checks instead of counting legal moves.
- Forgetting that en passant removes a pawn from a different square than the destination.
- A
Playerclass that holds a list of pieces in addition to the board: two sources of truth again.
If chess feels heavy, warm up with snake and ladder for game-loop structure, then come back. For how LLD rounds are evaluated at Indian product companies, see the LLD interview guide and the machine coding round guide.
Frequently Asked Questions
Level Up Your LLD Game
Prepflix's interview course takes you from parking lot to chess-level LLD problems with guided design reviews, so you learn when inheritance, composition and patterns actually pay off.
Explore the Prepflix Course →