Low-Level Design: Chess Full System Design & Object-Oriented Architecture
A low-level design (LLD) evaluation for a complex game like Chess tests an engineer's ability to transform ambiguous, high-density domain rules into clean, extensible, modular, and maintainable object-oriented code. This guide breaks down the end-to-end design of a Chess system—covering interview pacing strategies, core design patterns, incremental bottom-up code construction, state management, and real-world extensibility hooks.
1. Interview Strategy & Pacing Framework
In a typical 45 to 60-minute LLD interview (common at top tech companies like Uber, Confluent, and Google), attempting to design every feature of Chess—including complete move validation, AI engines, and complex edge cases—will lead to incomplete code. Success depends on disciplined time management and clear communication.
Outlining the Game Flow ("Enacting the Game")
Rather than jumping straight to class definitions, mentally simulate a live game between two players (e.g., Grandmasters on a stage):
- Setup: The board is initialized with two players sitting opposite each other. 32 pieces (16 White, 16 Black) are deployed to their standard starting cells on an grid.
- Turn Loop: White moves first. A player selects a piece, chooses a valid destination cell, executes the move, and captures an opponent piece if present.
- Validation & State Update: The board updates, logs the move, checks if the move places the opposing King under threat (Check), checks if any legal moves remain (Checkmate/Stalemate), and toggles turn control.
- Game Termination: The game terminates via Checkmate (Win/Loss), Stalemate (Draw), or mutual agreement/special rules.
Open vs. Closed Clarifying Questions
To define a tight, achievable scope, use open-ended questions to discover core expectations and closed-ended questions to confirm structural assumptions.
- Open-Ended Question: "What core requirements and move validation depth are you looking for in today's discussion?"
- Interviewer Response: Focus on key object models, turn management, modular move validation, game status detection, and clean OOP abstractions.
- Closed-Ended Question: "May I assume a standard 2-player human-vs-human game on an board, focusing on core pieces, move execution, and extensible validation interfaces?"
- Interviewer Response: Yes, that is aligned.
2. Chess Domain Fundamentals & Mechanics
Before modeling classes, establish a clear conceptual baseline for the rules and mechanics of Chess.
Piece Movement Breakdown
Victory & Game-End Conditions
- Check: The King is under immediate threat of capture by at least one opposing piece.
- Checkmate: The King is in check, and no legal move can remove the threat. Game ends in a victory for the attacking player.
- Stalemate: The player whose turn it is has no legal moves available and their King is not in check. Game ends in a draw.
- Draw by Special Rules: Three-fold repetition of position, 50-move rule (no pawn move or capture), or insufficient material to force checkmate.
Advanced Special Moves (Extensibility Hooks)
- Castling: Simultaneous move of the King (2 squares toward a Rook) and the Rook (moving to the square the King crossed), provided neither piece has previously moved and no squares crossed are under attack.
- En Passant: A diagonal capture by a pawn of an enemy pawn that just advanced two squares on its initial move, as if it had moved only one square.
- Pawn Promotion: A pawn reaching the opponent's back rank (8th rank for White, 1st for Black) must be promoted to a Queen, Rook, Bishop, or Knight.
3. Design Patterns Matrix
To ensure the architecture remains flexible, decoupled, and easily extensible, we apply five core Object-Oriented Design Patterns.
4. Incremental Architecture & Component Discovery
When presenting class diagrams and architecture in an interview, construct components bottom-up. Begin with fundamental, independent entities (like Piece and Cell) before building higher-level orchestrators (Board and ChessGame). Avoid drawing complex UML diagrams under strict time constraints; instead, outline modular class signatures and relationships directly in code or plain text.
5. Core Model Design: Pieces, Factories & Strategy Pattern
Strategy Pattern for Movement
Each piece delegates its movement logic to an implementation of MoveStrategy. This prevents Piece subclasses from bloating with spatial calculation code and enables dynamic behavior updates (e.g., custom chess variants or fairy pieces).
// MoveStrategy.java
public interface MoveStrategy {
boolean canMove(Cell start, Cell end, Board board);
}
// KingMoveStrategy.java
public class KingMoveStrategy implements MoveStrategy {
@Override
public boolean canMove(Cell start, Cell end, Board board) {
int rowDiff = Math.abs(start.getRow() - end.getRow());
int colDiff = Math.abs(start.getCol() - end.getCol());
// King can move 1 square in any direction
return (rowDiff <= 1 && colDiff <= 1) && !(rowDiff == 0 && colDiff == 0);
}
}
// QueenMoveStrategy.java
public class QueenMoveStrategy implements MoveStrategy {
@Override
public boolean canMove(Cell start, Cell end, Board board) {
int rowDiff = Math.abs(start.getRow() - end.getRow());
int colDiff = Math.abs(start.getCol() - end.getCol());
// Combines Rook (straight line) and Bishop (diagonal) rules
boolean isDiagonal = (rowDiff == colDiff);
boolean isStraight = (start.getRow() == end.getRow() || start.getCol() == end.getCol());
return isDiagonal || isStraight;
}
}
Abstract Piece and Concrete Implementations
// Piece.java
public abstract class Piece {
private final boolean white;
private boolean killed = false;
private MoveStrategy moveStrategy;
public Piece(boolean white, MoveStrategy moveStrategy) {
this.white = white;
this.moveStrategy = moveStrategy;
}
public boolean isWhite() {
return white;
}
public boolean isKilled() {
return killed;
}
public void setKilled(boolean killed) {
this.killed = killed;
}
public MoveStrategy getMoveStrategy() {
return moveStrategy;
}
public void setMoveStrategy(MoveStrategy moveStrategy) {
this.moveStrategy = moveStrategy;
}
public boolean canMove(Cell start, Cell end, Board board) {
if (end.getPiece() != null && end.getPiece().isWhite() == this.white) {
return false; // Cannot capture friendly piece
}
return moveStrategy.canMove(start, end, board);
}
}
// King.java
public class King extends Piece {
public King(boolean white) {
super(white, new KingMoveStrategy());
}
}
// Queen.java
public class Queen extends Piece {
public Queen(boolean white) {
super(white, new QueenMoveStrategy());
}
}
Piece Factory Pattern
The PieceFactory hides piece instantiation details and injects default strategies cleanly.
// PieceType.java
public enum PieceType {
KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN
}
// PieceFactory.java
public class PieceFactory {
public static Piece createPiece(PieceType type, boolean isWhite) {
switch (type) {
case KING:
return new King(isWhite);
case QUEEN:
return new Queen(isWhite);
// Additional pieces initialized here...
default:
throw new IllegalArgumentException("Unsupported Piece Type: " + type);
}
}
}
6. Board Architecture & Structural Models
Cell and Move Entities
// Cell.java
public class Cell {
private int row;
private int col;
private Piece piece;
public Cell(int row, int col, Piece piece) {
this.row = row;
this.col = col;
this.piece = piece;
}
public int getRow() { return row; }
public int getCol() { return col; }
public Piece getPiece() { return piece; }
public void setPiece(Piece piece) { this.piece = piece; }
}
// Move.java
public class Move {
private final Player player;
private final Cell start;
private final Cell end;
private final Piece pieceMoved;
private final Piece pieceKilled;
public Move(Player player, Cell start, Cell end, Piece pieceMoved, Piece pieceKilled) {
this.player = player;
this.start = start;
this.end = end;
this.pieceMoved = pieceMoved;
this.pieceKilled = pieceKilled;
}
public Cell getStart() { return start; }
public Cell getEnd() { return end; }
public Piece getPieceMoved() { return pieceMoved; }
public Piece getPieceKilled() { return pieceKilled; }
}
Singleton Board Class
// Board.java
public class Board {
private static Board instance;
private final Cell[][] cells = new Cell[8][8];
private Board() {
resetBoard();
}
// Thread-safe Singleton Access
public static synchronized Board getInstance() {
if (instance == null) {
instance = new Board();
}
return instance;
}
public Cell getCell(int row, int col) {
if (row < 0 || row >= 8 || col < 0 || col >= 8) {
throw new IndexOutOfBoundsException("Invalid Board Position: (" + row + ", " + col + ")");
}
return cells[row][col];
}
public void resetBoard() {
// Initialize empty cells
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
cells[i][j] = new Cell(i, j, null);
}
}
// Place Major Pieces
cells[0][3] = new Cell(0, 3, PieceFactory.createPiece(PieceType.QUEEN, true));
cells[0][4] = new Cell(0, 4, PieceFactory.createPiece(PieceType.KING, true));
cells[7][3] = new Cell(7, 3, PieceFactory.createPiece(PieceType.QUEEN, false));
cells[7][4] = new Cell(7, 4, PieceFactory.createPiece(PieceType.KING, false));
// Pawns, Rooks, Knights, Bishops populated incrementally...
}
}
7. Game Controller & Execution Engine
Player Entity & Status Enumeration
// GameStatus.java
public enum GameStatus {
ACTIVE, BLACK_WIN, WHITE_WIN, FORFEIT, STALEMATE
}
// Player.java
public class Player {
private final String name;
private final boolean whiteSide;
public Player(String name, boolean whiteSide) {
this.name = name;
this.whiteSide = whiteSide;
}
public String getName() { return name; }
public boolean isWhiteSide() { return whiteSide; }
}
The Turn Execution Workflow
Master Game Orchestrator (ChessGame)
// ChessGame.java
import java.util.ArrayList;
import java.util.List;
public class ChessGame {
private final Board board;
private final Player player1;
private final Player player2;
private Player currentTurn;
private GameStatus status;
private final List<Move> movesPlayed;
public ChessGame(Player p1, Player p2) {
this.player1 = p1;
this.player2 = p2;
this.board = Board.getInstance();
this.movesPlayed = new ArrayList<>();
// White traditionally moves first
this.currentTurn = p1.isWhiteSide() ? p1 : p2;
this.status = GameStatus.ACTIVE;
}
public boolean makeMove(Player player, int startX, int startY, int endX, int endY) {
if (this.status != GameStatus.ACTIVE) {
System.out.println("Game is completed. No further moves allowed.");
return false;
}
if (player != currentTurn) {
System.out.println("Error: Out of turn move attempt by " + player.getName());
return false;
}
Cell startCell = board.getCell(startX, startY);
Cell endCell = board.getCell(endX, endY);
Piece sourcePiece = startCell.getPiece();
if (sourcePiece == null || sourcePiece.isWhite() != player.isWhiteSide()) {
System.out.println("Error: Invalid selection of source square.");
return false;
}
if (!sourcePiece.canMove(startCell, endCell, board)) {
System.out.println("Error: Invalid move trajectory for piece " + sourcePiece.getClass().getSimpleName());
return false;
}
Piece destPiece = endCell.getPiece();
if (destPiece != null) {
destPiece.setKilled(true);
}
// Execute Move
Move move = new Move(player, startCell, endCell, sourcePiece, destPiece);
movesPlayed.add(move);
endCell.setPiece(sourcePiece);
startCell.setPiece(null);
// Check Victory Trigger (Simplified: King Capture)
if (destPiece instanceof King) {
this.status = player.isWhiteSide() ? GameStatus.WHITE_WIN : GameStatus.BLACK_WIN;
System.out.println("Checkmate! Winner: " + player.getName());
return true;
}
// Switch Turn
this.currentTurn = (this.currentTurn == player1) ? player2 : player1;
return true;
}
public GameStatus getStatus() { return status; }
}
8. Real-World Interview Edge Cases & Architectural Pitfalls
Decoupling Piece Location from Board Storage
- Pitfall: Storing
rowandcoldirectly inside thePieceobject introduces bi-directional dependency bugs and synchronization issues between the piece and the board cell. - Best Practice: Treat
Pieceas a stateless object relative to coordinates. Coordinate state belongs strictly toCell. To locate a piece, query theBoardor query theCell.
Concurrency and Thread Safety
- In multi-threaded online chess platforms, multiple threads may attempt simultaneous reads and writes to the
Boardinstance. - Mitigation: Use double-checked locking or an initialization-on-demand holder idiom for the
BoardSingleton. Ensure turn state mutations insidemakeMove()are wrapped in critical sections (synchronizedblocks or reentrant locks per game session).
// Thread-Safe Bill Pugh Singleton Pattern for Board
public class Board {
private Board() { resetBoard(); }
private static class BoardHolder {
private static final Board INSTANCE = new Board();
}
public static Board getInstance() {
return BoardHolder.INSTANCE;
}
}
Full Move Validation Complexity vs. Scope Pacing
Interview Warning Callout: Writing code for check validation, checkmate detection, pinned pieces, and path-blocking line-of-sight checks can take hours.
When interviewing:
- Express clearly to the interviewer that real-world move validation involves path scanning ( along diagonals/rank/file) and simulated move testing (checking if a move exposes one's own King to check).
- Explicitly encapsulate validation within the
MoveStrategyinterface. - Stub complex checks during initial coding phase to ensure a functional, compilable game loop before time runs out.
9. Advanced Extensibility Scenarios
During the final 10–15 minutes of an interview, interviewers often ask for extensions to test the flexibility of your architecture.
Extension 1: Arbitrary Board Sizes ()
Because Board encapsulates its 2D grid logic and Cell handles bounds checking, expanding to non-standard board sizes (e.g., or ) requires minimal code modification.
// FlexBoard.java
public class FlexBoard {
private final int rows;
private final int cols;
private final Cell[][] cells;
public FlexBoard(int rows, int cols) {
this.rows = rows;
this.cols = cols;
this.cells = new Cell[rows][cols];
initialize();
}
private void initialize() {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
cells[r][c] = new Cell(r, c, null);
}
}
}
}
Extension 2: Event Notification via Observer Pattern
Broadcast moves, state changes, and check warnings to external services (e.g., UI updates, analytics engines, chat systems).
// GameEventListener.java
public interface GameEventListener {
void onMoveMade(Move move);
void onGameStateChanged(GameStatus newStatus);
}
// ConsoleGameListener.java
public class ConsoleGameListener implements GameEventListener {
@Override
public void onMoveMade(Move move) {
System.out.println("[EVENT LOG] Move Executed: "
+ move.getPieceMoved().getClass().getSimpleName()
+ " to (" + move.getEnd().getRow() + ", " + move.getEnd().getCol() + ")");
}
@Override
public void onGameStateChanged(GameStatus newStatus) {
System.out.println("[EVENT LOG] Game Status Shifted to: " + newStatus);
}
}
Extension 3: Player Decision Strategies (Human vs. AI)
Leverage the Strategy Pattern for player moves to support human input parsing alongside automated AI move selection.
// PlayerStrategy.java
public interface PlayerStrategy {
Move makeDecision(Board board, boolean isWhite);
}
// HumanPlayerStrategy.java (Parses Algebraic Notation e.g., "e2e4")
public class HumanPlayerStrategy implements PlayerStrategy {
@Override
public Move makeDecision(Board board, boolean isWhite) {
// Reads from console input, parses notation string, returns Move object
return null; // Stubbed for brevity
}
}
// AIPlayerStrategy.java (Evaluates Legal Moves Randomly or via Minimax Engine)
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class AIPlayerStrategy implements PlayerStrategy {
@Override
public Move makeDecision(Board board, boolean isWhite) {
List<Move> legalMoves = generateLegalMoves(board, isWhite);
if (legalMoves.isEmpty()) return null;
// Selects random valid move (or executes Minimax decision algorithm)
return legalMoves.get(new Random().nextInt(legalMoves.size()));
}
private List<Move> generateLegalMoves(Board board, boolean isWhite) {
// Scans board, aggregates valid destinations for current player
return new ArrayList<>();
}
}
Summary and Key Takeaways
- Structured Pacing Wins Interviews: Allocate explicit time blocks for requirements, entity identification, clean implementation, and extensibility. Never jump straight into code without outlining the core game flow.
- Apply Design Patterns Judiciously:
- Strategy Pattern: Isolates piece movement rules (
MoveStrategy) and player input sources (PlayerStrategy). - Singleton Pattern: Guarantees a single source of truth for the physical
Board. - Factory Pattern: Centralizes complex piece instantiation (
PieceFactory). - Observer Pattern: Decouples UI rendering, logging, and notifications from the core game engine.
- Strategy Pattern: Isolates piece movement rules (
- Build Bottom-Up: Begin coding low-level primitives (
Piece,Cell,Move) before constructing higher-level aggregates (Board,ChessGame). - Decouple Coordinates from Domain Objects: Avoid embedding raw grid coordinates directly in stateless
Pieceobjects. Keep coordinate ownership strictly withinCellentities. - Manage Complexity Explicitly: Stub high-complexity validation rules (e.g., path-blocking, checkmate algorithms) early on. Highlight them as modular interfaces to maintain code completeness within time constraints.
- Design for Extensibility: A well-structured system supports larger board dimensions, alternative player input strategies (AI/Human), and real-time event broadcasting with minimal modifications to existing code.