Design Chess
A full low-level design for a chess engine: per-piece move validation via Strategy, check/checkmate detection, special moves, and undo via the Command pattern.
Design Chess
Chess is the LLD interview problem that rewards resisting one specific temptation: writing a single giant isValidMove() method with a switch on piece type. The moment you see "six piece types, each with completely different movement rules," that's a Strategy-pattern signal, not a conditional-chain signal. This guide works the problem end-to-end using the six-step framework used throughout this phase: requirements → actors → class diagram → class design → patterns → algorithms/concurrency → trade-offs.
1. Requirements
Functional:
- Standard 8×8 board, six piece types (king, queen, rook, bishop, knight, pawn), two players (white/black).
- Validate that a proposed move is legal for the specific piece making it, accounting for board occupancy and check state.
- Detect check, checkmate, and stalemate after every move.
- Support special moves: castling (kingside/queenside), en passant, pawn promotion.
- Record full move history; support undoing the last move.
Non-functional:
- Move validation must not allow a move that leaves the moving player's own king in check (this is the rule most naive implementations get wrong first).
- The design should make it straightforward to add a new game mode (e.g. a variant with different starting positions) without rewriting move-validation logic per piece.
2. Actors & Use Cases
| Actor | Use cases |
|---|---|
| Player (White / Black) | Propose a move, request undo, resign |
| Game | Enforce turn order, validate moves, detect terminal states, record history |
3. Class Diagram
Piece holds a MoveStrategy rather than implementing canMove directly in each subclass with inheritance-based overriding — this is a deliberate Strategy-over-inheritance choice. It means a "Fairy Chess" variant (custom piece movement rules) can inject a different MoveStrategy into a standard Piece shell without subclassing, and it keeps movement-shape logic testable in complete isolation from the Piece class itself.
4. Core Class Design (Java)
MoveStrategy per piece type
interface MoveStrategy {
boolean isValidMove(Board board, Position from, Position to);
}
final class RookMoveStrategy implements MoveStrategy {
public boolean isValidMove(Board board, Position from, Position to) {
if (from.row() != to.row() && from.col() != to.col()) return false; // must be straight line
return board.isPathClear(from, to); // no pieces in between
}
}
final class KnightMoveStrategy implements MoveStrategy {
public boolean isValidMove(Board board, Position from, Position to) {
int rowDiff = Math.abs(from.row() - to.row());
int colDiff = Math.abs(from.col() - to.col());
return (rowDiff == 2 && colDiff == 1) || (rowDiff == 1 && colDiff == 2);
// note: knight is the only piece that never needs a path-clear check
}
}
final class BishopMoveStrategy implements MoveStrategy {
public boolean isValidMove(Board board, Position from, Position to) {
int rowDiff = Math.abs(from.row() - to.row());
int colDiff = Math.abs(from.col() - to.col());
if (rowDiff != colDiff) return false; // must be diagonal
return board.isPathClear(from, to);
}
}
// Queen composes Rook + Bishop logic rather than duplicating it
final class QueenMoveStrategy implements MoveStrategy {
private final MoveStrategy rook = new RookMoveStrategy();
private final MoveStrategy bishop = new BishopMoveStrategy();
public boolean isValidMove(Board board, Position from, Position to) {
return rook.isValidMove(board, from, to) || bishop.isValidMove(board, from, to);
}
}QueenMoveStrategy delegating to Rook + Bishop instead of reimplementing "straight or diagonal" logic is composition doing real work: it eliminates an entire class of bugs where the queen's movement rules drift out of sync with the rook's or bishop's after a future fix.
Piece and the abstract base
abstract class Piece {
protected final Color color;
protected Position position;
protected final MoveStrategy moveStrategy;
protected Piece(Color color, Position position, MoveStrategy moveStrategy) {
this.color = color;
this.position = position;
this.moveStrategy = moveStrategy;
}
boolean canMove(Board board, Position to) {
Piece target = board.pieceAt(to);
if (target != null && target.color == this.color) return false; // can't capture own piece
return moveStrategy.isValidMove(board, position, to);
}
}Move validation including "does this leave my own king in check"
final class MoveValidator {
boolean isLegal(Game game, Position from, Position to) {
Piece piece = game.getBoard().pieceAt(from);
if (piece == null || piece.getColor() != game.getCurrentTurn()) return false;
if (!piece.canMove(game.getBoard(), to)) return false;
// Simulate the move on a scratch copy of the board — this is the check
// most naive chess implementations skip, and it's the one that matters most.
Board scratch = game.getBoard().copy();
scratch.movePiece(from, to);
return !isKingInCheck(scratch, piece.getColor());
}
private boolean isKingInCheck(Board board, Color kingColor) {
Position kingPos = board.findKing(kingColor);
return board.allPieces(kingColor.opposite()).stream()
.anyMatch(enemyPiece -> enemyPiece.canMove(board, kingPos));
}
}This simulate-then-check step is the single most commonly missed requirement in chess LLD interviews. Without it, a player could legally move a piece that was the only thing blocking their own king from check — a rule violation that looks fine until someone actually plays the implementation.
Undo via Command pattern
interface Command {
void execute();
void undo();
}
final class MoveCommand implements Command {
private final Board board;
private final Position from, to;
private Piece capturedPiece; // remembered for undo
MoveCommand(Board board, Position from, Position to) {
this.board = board; this.from = from; this.to = to;
}
public void execute() {
capturedPiece = board.pieceAt(to);
board.movePiece(from, to);
}
public void undo() {
board.movePiece(to, from);
if (capturedPiece != null) board.placePiece(capturedPiece, to);
}
}
final class Game {
private final Deque<Command> history = new ArrayDeque<>();
void makeMove(Position from, Position to) {
Command move = new MoveCommand(board, from, to);
move.execute();
history.push(move);
switchTurn();
}
void undoLastMove() {
if (!history.isEmpty()) {
history.pop().undo();
switchTurn();
}
}
}5. Design Patterns Applied
| Pattern | Where used | Why |
|---|---|---|
| Strategy | MoveStrategy per piece type | Each piece's movement rule is isolated, testable, and reusable (Queen composes Rook + Bishop) |
| Command | MoveCommand with execute()/undo() | Clean undo/redo without special-casing board-state rollback logic |
| State | GameStatus (in progress, check, checkmate, stalemate, draw) | Terminal-state detection is a state machine, not a scattered set of booleans |
| Factory | PieceFactory for standard board setup | Isolates board-initialization logic from Game, easy to swap for variant setups |
6. Key Algorithms, Concurrency & Edge Cases
Checkmate detection
Checkmate is: the current player's king is in check, AND no legal move exists that removes the check. This means checkmate detection reuses MoveValidator.isLegal across every possible move for every piece the player controls:
boolean isCheckmate(Game game, Color color) {
if (!isKingInCheck(game.getBoard(), color)) return false; // not even in check
return game.getBoard().allPieces(color).stream()
.flatMap(piece -> game.getBoard().allPositions().stream()
.filter(to -> new MoveValidator().isLegal(game, piece.getPosition(), to)))
.findAny()
.isEmpty(); // no legal move exists anywhere on the board
}This brute-force "try every piece against every square" approach is O(pieces × 64) isLegal calls, each of which does a board-copy simulation — acceptable for a human-paced game, but far too slow for a chess engine evaluating millions of positions per second. If the interviewer pushes on performance, this is the moment to discuss move generation optimizations (bitboards, pre-computed attack tables) as a follow-up, not a first-pass requirement.
Special moves
| Move | Trigger condition | Implementation note |
|---|---|---|
| Castling | King and chosen rook have never moved, no pieces between them, king not currently in check and doesn't pass through check | Track a hasMoved flag on King/Rook; validate the king's path square-by-square via isKingInCheck simulation |
| En passant | Opponent pawn just moved two squares, landing adjacent to your pawn | Only legal on the immediately following move — track "last move was a two-square pawn advance" on Game |
| Promotion | A pawn reaches the last rank | MoveCommand replaces the pawn Piece with the chosen promoted piece as part of execute() |
Board representation trade-off
| Representation | Pros | Cons |
|---|---|---|
2D array (Piece[8][8]) | Simple, readable, matches the problem's mental model | O(n) findKing, allPieces scans |
| Bitboard (64-bit long per piece type/color) | Extremely fast move generation via bitwise ops | Non-obvious to implement/debug; overkill unless building a real engine |
For an LLD interview, lead with the 2D array — it's what the interviewer wants to see you reason clearly about. Mention bitboards as the answer to "how would you make this faster for an engine," not as your first design.
7. Trade-offs & Extensions
- Immutable
Movehistory vs. mutable board replay: recording fullMoveobjects (not just from/to) lets you reconstruct any board state without replaying the whole game, at the cost of slightly more memory per move. - Extensions: chess clock / timeout handling, PGN export, a
MoveValidatorthat also returns why a move is illegal (useful for a UI hint system), AI opponent via a pluggableMoveStrategy-adjacentEvaluatorinterface.
Interview Questions
- Why is a Strategy-per-piece-type design preferable to a single method with a switch on piece type?
- Walk through exactly how you detect that a proposed move would leave the mover's own king in check.
- How does checkmate detection differ from simple check detection?
- How would you implement castling's "king cannot pass through check" rule?
- Why does en passant require tracking state beyond the current board position?
- What would you change about this design to make it fast enough for a chess engine evaluating millions of positions per second?