06-lld-interview-problems

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.

August 11, 2026
lldchessgamevalidationstrategy-patternmove-history

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

ActorUse cases
Player (White / Black)Propose a move, request undo, resign
GameEnforce 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

java
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

java
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"

java
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

java
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

PatternWhere usedWhy
StrategyMoveStrategy per piece typeEach piece's movement rule is isolated, testable, and reusable (Queen composes Rook + Bishop)
CommandMoveCommand with execute()/undo()Clean undo/redo without special-casing board-state rollback logic
StateGameStatus (in progress, check, checkmate, stalemate, draw)Terminal-state detection is a state machine, not a scattered set of booleans
FactoryPieceFactory for standard board setupIsolates 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:

java
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

MoveTrigger conditionImplementation note
CastlingKing and chosen rook have never moved, no pieces between them, king not currently in check and doesn't pass through checkTrack a hasMoved flag on King/Rook; validate the king's path square-by-square via isKingInCheck simulation
En passantOpponent pawn just moved two squares, landing adjacent to your pawnOnly legal on the immediately following move — track "last move was a two-square pawn advance" on Game
PromotionA pawn reaches the last rankMoveCommand replaces the pawn Piece with the chosen promoted piece as part of execute()

Board representation trade-off

RepresentationProsCons
2D array (Piece[8][8])Simple, readable, matches the problem's mental modelO(n) findKing, allPieces scans
Bitboard (64-bit long per piece type/color)Extremely fast move generation via bitwise opsNon-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 Move history vs. mutable board replay: recording full Move objects (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 MoveValidator that also returns why a move is illegal (useful for a UI hint system), AI opponent via a pluggable MoveStrategy-adjacent Evaluator interface.

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?