Design a Chess Game
OOP design for a two-player chess game covering piece hierarchy via inheritance, move validation via polymorphism, check/checkmate detection, and game state coordination.
The Problem
You are building an online board game platform. The first game you need to support is standard two-player chess. Players take turns moving pieces according to standard chess rules, and the system must detect illegal moves, check, and checkmate. The current prototype has all logic in a single 2000-line class with switch statements for every piece type. Adding new rules or piece variants is nearly impossible without breaking existing logic.
Chess is a favorite LLD interview question because it forces you to make real OOP decisions: abstract classes vs interfaces, composition vs inheritance, where to put validation logic, and how to handle the interaction between game rules and board state. The design choices you make here apply directly to any turn-based game system.
Design the core classes for a chess game that handles piece movement, turn management, and check/checkmate detection.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions. In a chess interview, the most important question is about scope: does the interviewer want the full FIDE ruleset or just the basic mechanics?
You: "Are we designing a two-player game only, or do we need support for a computer opponent?"
Interviewer: "Two human players for now. A computer opponent is a good extension to mention later."
Good. You have confirmed the core interaction model. Now nail down the rules.
You: "Should we support castling, en passant, and pawn promotion?"
Interviewer: "Start with basic piece movement and check/checkmate. Mention those as extensions."
That simplifies the scope significantly. Special moves add complexity to both Piece subclasses and Game coordination. Ruling them out lets you focus on the core hierarchy.
You: "What happens when a player attempts an illegal move, like moving into check?"
Interviewer: "Reject the move cleanly and let the same player try again. Don't corrupt the board state."
You: "How should the game end? Only checkmate, or do we need stalemate and draw conditions too?"
Interviewer: "Checkmate ends the game. Mention stalemate as an extension."
You: "Do we need to persist game state or support undo/redo?"
Interviewer: "No persistence. Undo is a good follow-up question."
You: "Should the system expose board state for a UI, or is this purely backend logic?"
Interviewer: "Expose enough state for a UI to render, but don't build the UI itself."
That last question matters because exposing board state means we need a read-only view of the 8x8 grid. We will add a getBoardState() method to Board rather than letting the UI reach into internals.
You: "Should the design support multiple concurrent games?"
Interviewer: "Each Game instance is independent. No shared state between games."
Perfect. You have clarified scope and ruled out unnecessary complexity.
Final Requirements
Now distill the conversation into a structured requirements list. Number them so you can reference them later when deriving classes.
Functional Requirements:
- An 8x8 board initialized with 16 pieces per player in standard starting positions
- Each piece type (King, Queen, Rook, Bishop, Knight, Pawn) follows its own movement rules
- Players alternate turns, starting with White
- A move is rejected if the piece cannot legally reach the target square
- A move is rejected if it would leave the moving player's own king in check
- The game detects checkmate (current player has no legal moves and king is in check)
Non-Functional Requirements:
- Each Game instance is independent (no shared mutable state)
- Adding a new piece type requires only a new subclass, not changes to existing code
- Board state remains consistent even when invalid moves are attempted
Out of Scope:
- Castling, en passant, pawn promotion
- Stalemate and draw conditions (50-move rule, threefold repetition)
- Game persistence or serialization
- Timers and clocks
- UI rendering
- Computer opponent / AI
Example Inputs and Outputs
Scenario 1: Valid pawn opening move
- Input: White moves pawn from e2 to e4
- Expected: Move succeeds, turn switches to Black
- Why: Validates basic piece movement (pawn can move two squares from starting row)
Scenario 2: Invalid move blocked by own piece
- Input: White tries to move rook from a1 to a2 (with a pawn still on a2)
- Expected: Move rejected, turn stays with White
- Why: Validates that pieces cannot move through or capture their own pieces
Scenario 3: Move that puts own king in check
- Input: White's bishop is pinned to the king by Black's rook. White tries to move the bishop.
- Expected: Move rejected, turn stays with White
- Why: Validates the "no self-check" rule: every move must be tested against the player's own king safety
These three scenarios cover the three layers of move validation: piece-level rules (can the piece reach that square?), board-level rules (is the path clear?), and game-level rules (does the move leave the king safe?). Every valid move must pass all three layers.
Scenarios as test cases
In a real interview, write these scenarios on the whiteboard. They serve as acceptance criteria for your design. After you finish coding, trace each scenario through your classes to prove correctness. Interviewers love seeing this discipline.
Try It Yourself
Try it yourself
Before reading the solution, spend 15-20 minutes sketching your own class diagram. Focus on one question: what does each piece type need to know to decide if a move is legal? Think about where check detection logic should live. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look at the nouns in your requirements: game, board, piece, position, player, color, state. Each noun is a candidate entity. Now filter: which ones have distinct responsibilities?
A common mistake is putting everything in one giant ChessGame class. Good design means each class has a single, clear job. Another mistake is over-splitting: you do not need a Square class when a Position record gives you the same thing with zero overhead. A third mistake is creating a Move class too early: for the basic scope, Position from and Position to are sufficient parameters.
| Entity | Responsibility | Key Attributes |
|---|---|---|
| Game | Orchestrator. Manages turns, validates moves against game rules, detects checkmate. | board, currentTurn, gameState |
| Board | The 8x8 grid. Owns piece placement, removal, and lookup. Has no knowledge of game rules. | squares (Piece[][]) |
| Piece (abstract) | Base type for all chess pieces. Each subclass defines its own movement rules via polymorphism. | color, position |
| Position (record) | Immutable value object representing a row/column coordinate on the board. | row, col |
| Color (enum) | Distinguishes the two sides. Used for turn management and piece ownership. | WHITE, BLACK |
| GameState (enum) | Tracks whether the game is active, won, or drawn. Controls whether moves are accepted. | ACTIVE, WHITE_WIN, BLACK_WIN, STALEMATE |
Notice we separated Game from Board because they have different responsibilities. Game manages the rules of chess (turns, check, checkmate). Board manages the physical grid (what piece is where). Merging them violates SRP because adding a new game rule would force changes to grid management code.
We also separated Piece into an abstract class rather than a concrete class with a type field. This is the single most important design decision in the entire problem. With an abstract class, each piece subclass is a self-contained unit that knows its own movement rules. With a type field, every method that cares about piece behavior needs a switch statement.
We skipped a Player class. In this scoped design, Color already distinguishes the two sides, and there is no player-specific state (no name, no rating, no timer). If the interviewer asks about player profiles later, that is when Player becomes its own entity.
Interview tip: entity identification
In an interview, write these entities on the whiteboard before drawing any relationships. Interviewers want to see that you can decompose a problem into manageable pieces. Listing 5-7 entities with one-line responsibilities takes 2 minutes and sets the foundation for everything else.
Step 2: Define Relationships and Class Design
Now that we have our entities, define how they connect. The class diagram shows the static structure. Then, for each major class, derive its state and methods directly from the requirements. This derivation process is the core teaching tool: it shows how to go from requirements to code systematically, not by guessing.
Class Diagram
The class diagram below shows all entities and their relationships. Pay attention to the hierarchy: six concrete piece types inherit from the abstract Piece class. Game owns one Board (composition). Board contains many Pieces (composition). Position, Color, and GameState are value types with no outgoing relationships.
Deriving the Game Class
Game is the orchestrator. Every functional requirement maps to something Game must do. In an interview, always derive the main orchestrator class first because it reveals what helper classes need to support.
Deriving state from requirements:
| Requirement | What Game must track |
|---|---|
| "Players alternate turns, starting with White" | currentTurn (Color) |
| "The game detects checkmate" | state (GameState) |
| "8x8 board with pieces" | board (Board) |
Deriving methods from needs:
| Need | Method |
|---|---|
| "Player makes a move" | makeMove(Position from, Position to): boolean |
| "Move rejected if it leaves own king in check" | isInCheck(Color): boolean |
| "Detect checkmate" | isCheckmate(Color): boolean |
| "Expose game state for UI" | getState(): GameState |
Deriving the Board Class
Board is the grid manager. It knows where pieces are, but not the rules of chess. This boundary is critical: if Board starts validating legality, you lose the ability to test game rules independently of grid mechanics.
Deriving state from requirements:
| Requirement | What Board must track |
|---|---|
| "8x8 board with pieces" | squares (Piece[8][8]) |
| "Standard starting positions" | initialize() method |
Deriving methods from needs:
| Need | Method |
|---|---|
| "Check what piece is at a square" | pieceAt(Position): Piece |
| "Move a piece from A to B" | movePiece(Position, Position): Piece (returns captured piece) |
| "Undo a move if it causes self-check" | undoMove(Position, Position, Piece) |
| "Find the king for check detection" | findKing(Color): Position |
| "Get all pieces of a color for checkmate scan" | getAllPieces(Color): List<Piece> |
Key design decision
movePiece returns the captured piece (or null). This lets Game store the captured piece before calling isInCheck, and pass it back to undoMove if the move is illegal. No need for a separate capture tracking system.
A key relationship decision: Board does NOT validate whether a move is legal. It blindly moves pieces when told to. This is intentional. Game handles legality (turn order, check validation). Board handles grid mechanics (swap squares, return captured piece). If Board also validated legality, we would have game rules split across two classes with no clear boundary.
Another relationship: Piece references Board (as a parameter to getLegalMoves) but does not own it. This avoids circular ownership. Board owns pieces (composition). Pieces ask Board questions (dependency) to compute their moves. This dependency direction is important: Board does not depend on any specific Piece subclass. It stores Piece references and calls the abstract getLegalMoves() method. This means you can add a new piece type without touching Board.
Deriving the Piece Hierarchy
Each piece type is a subclass of Piece with a single abstract method: getLegalMoves(Board). This is the heart of the polymorphism. Game calls piece.getLegalMoves(board) without knowing or caring what type of piece it is.
Why abstract class instead of interface? Pieces share state (color, position) and behavior (getters). An abstract class lets us define those once. If we used an interface, every subclass would duplicate the same fields and constructors.
I have seen candidates use a Movable interface with getLegalMoves() and then a separate abstract Piece class that implements it. In theory this is more flexible, but in practice no chess entity other than a Piece needs to be Movable. The extra interface adds indirection without value. Keep it simple: one abstract class.
The six concrete subclasses (Rook, Knight, Bishop, Queen, King, Pawn) each override getLegalMoves(). Among these, Rook, Bishop, and Queen share a "sliding" movement pattern: they move along a set of directions until blocked. This shared logic is extracted into the slide() helper on the abstract Piece class (Template Method pattern, discussed in Step 3).
Knight, King, and Pawn each have unique movement logic that does not fit the sliding pattern. Knight jumps to fixed offsets. King moves one step in any direction. Pawn moves forward, captures diagonally, and has a special two-step opening move. These are implemented directly in each subclass without shared helpers.
Final Class Design (Pseudocode Summary)
record Position(row, col)
isValid() -> row in [0,7] and col in [0,7]
enum Color { WHITE, BLACK }
enum GameState { ACTIVE, WHITE_WIN, BLACK_WIN, STALEMATE }
abstract class Piece(color, position)
abstract getLegalMoves(board) -> List<Position>
class Rook extends Piece // slides horizontally/vertically
class Bishop extends Piece // slides diagonally
class Queen extends Piece // slides in all 8 directions
class Knight extends Piece // L-shaped jumps
class King extends Piece // one square in any direction
class Pawn extends Piece // forward one (or two from start), diagonal capture
class Board
squares: Piece[8][8]
initialize(), pieceAt(), movePiece(), undoMove(), findKing(), getAllPieces()
class Game
board, currentTurn, state
makeMove(), isInCheck(), isCheckmate()
Interview tip: the pseudocode blueprint
At this point in an interview, you should have this entire design on the whiteboard: entities, relationships, and class signatures. The interviewer can see at a glance that your Piece hierarchy uses polymorphism, your Board is a dumb grid, and your Game orchestrates the rules. This is the "blueprint moment" where you pause and ask: "Does this structure make sense before I start implementing?" Good candidates check for buy-in before writing code.
Step 3: Choose Design Patterns
Two patterns emerge naturally from our class design. The first handles how each piece type defines its movement. The second handles how sliding pieces (Rook, Bishop, Queen) share common traversal logic. In an interview, always explain WHY you chose a pattern, not just which pattern you picked. Naming a pattern is worth 1 point. Explaining the signal that led you to it is worth 10.
Pattern 1: Inheritance Hierarchy for Piece Movement
The signal: Six piece types, each with completely different movement rules, but all sharing a common interface (getLegalMoves). This is textbook polymorphism.
Why inheritance over other approaches: The alternatives are worse. A switch statement in Board or Game that checks piece type violates OCP: every new piece type forces edits to existing code. An enum with behavior methods works but lacks the ability to share state (color, position) cleanly. An abstract class gives us shared state, a shared constructor, and a single abstract method that each subclass implements.
In my experience, chess is one of the cleanest demonstrations of inheritance in LLD interviews. The piece hierarchy is intuitive for interviewers to follow, and it maps directly to the real-world domain. If you are asked "where would you use inheritance in a real project?" remember chess pieces.
How it maps to our entities:
- Abstract class =
Piece(shared color, position, getters) - Concrete subclasses =
Rook,Knight,Bishop,Queen,King,Pawn
Pattern 2: Template Method for Sliding Pieces
The signal: Rook, Bishop, and Queen all "slide" along directions until hitting the board edge or another piece. The only difference is which directions they slide in. This is duplicated logic waiting to be extracted.
Why Template Method: The sliding algorithm is identical: start at the piece's position, step in a direction, collect valid squares, stop when blocked. Only the direction array differs. We extract the sliding logic into a helper method on the abstract Piece class and let each sliding piece call it with its own directions.
This means Queen's entire getLegalMoves() method is a one-liner: return slide(board, ALL_EIGHT_DIRECTIONS). That is the sign of a well-extracted template. If you later need to add a "Chancellor" piece (Rook + Knight), its sliding half is also a one-liner.
Bad / Better / Best: Piece Movement Design
Interview tip: pattern justification
When you present the inheritance hierarchy and Template Method in an interview, always tie them back to the requirements. Say: "Requirement 2 says each piece type follows its own movement rules. That maps directly to an abstract getLegalMoves() with six concrete implementations. The interviewer can verify this mapping instantly." Do not just say "I used inheritance here." Explain the signal that led to the pattern.
Summary of Patterns Used
| Pattern | Applied To | Signal in Requirements |
|---|---|---|
| Inheritance hierarchy | Piece -> Rook, Knight, Bishop, Queen, King, Pawn | "Each piece type follows its own movement rules" |
| Template Method | slide() shared by Rook, Bishop, Queen | Three pieces share identical traversal logic with different directions |
| Polymorphism | getLegalMoves(Board) on every piece | Game calls the same method on any piece without knowing its type |
Step 4: Build the Solution
This is the section interviewers spend the most time on. You have already identified entities, drawn the class diagram, and chosen patterns. Now translate that design into working code. Start with pseudocode for the key methods, then write the full implementation.
makeMove(): The Core Logic
This is the method interviewers will ask you to write first. It coordinates turn validation, piece movement, and check detection. Every other method exists to support this one.
Core logic (happy path):
- Get the piece at the source position
- Verify it belongs to the current player (turn enforcement)
- Verify the target is in the piece's legal moves (piece-level validation)
- Execute the move on the board, capturing if occupied
- Check if the move leaves our own king in check (game-level validation)
- If self-check, undo the move and reject it
- Check if the opponent is now in checkmate (endgame detection)
- Switch turns to the opponent
Edge cases (reject before touching state):
- No piece at the source position
- Piece belongs to the other player (wrong turn)
- Target position not in the piece's legal moves
- Move would leave own king in check
- Game is already over (checkmate or stalemate)
Handle these as guard clauses at the top of the method. Each returns false immediately without touching the board. This "fail fast" pattern keeps the happy path clean and ensures board state is never corrupted by a partially-applied invalid move.
function makeMove(from, to):
if state != ACTIVE: return false
piece = board.pieceAt(from)
if piece == null or piece.color != currentTurn: return false
if to not in piece.getLegalMoves(board): return false
captured = board.movePiece(from, to)
if isInCheck(currentTurn):
board.undoMove(from, to, captured) // restore board state
return false
opponent = opposite(currentTurn)
if isCheckmate(opponent):
state = (currentTurn == WHITE) ? WHITE_WIN : BLACK_WIN
currentTurn = opponent
return true
In an interview, I always write this pseudocode first. It shows the interviewer you understand the flow before you get into syntax details. Notice the guard clause pattern: every edge case returns early, and the happy path continues through. This is cleaner than deeply nested if-else blocks.
Design alternative: Some candidates throw exceptions instead of returning false. In an interview, a boolean return keeps things simpler. If the interviewer asks about error reporting, explain that you would return a MoveResult enum (SUCCESS, INVALID_PIECE, WRONG_TURN, SELF_CHECK) instead of a boolean. But start simple.
Common trap: forgetting to undo
If you move the piece, detect self-check, but forget to undo, the board is corrupted. Always pair movePiece with undoMove on the failure path. Some candidates use board cloning instead, which is cleaner but allocates more memory. Both approaches are valid; pick one and be consistent.
isInCheck(): Check Detection
Check detection is the second most important method. Every call to makeMove() invokes it to verify the player did not put their own king in danger. It is also called by isCheckmate() to test whether any escape move works.
Core logic:
- Find the king's position for the given color
- Get all opponent pieces
- For each opponent piece, compute its legal moves
- If any opponent piece can reach the king's position, that color is in check
function isInCheck(color):
kingPos = board.findKing(color)
opponent = opposite(color)
for each piece in board.getAllPieces(opponent):
if kingPos in piece.getLegalMoves(board):
return true
return false
Notice something important: isInCheck asks each opponent piece for its legal moves, not its "attack squares." For most pieces these are the same. But for Pawns, they differ: a pawn attacks diagonally but moves forward. In our scoped design (no en passant), this still works because a pawn's getLegalMoves() already includes diagonal capture squares when an enemy is there. If the king is on a diagonal, the pawn's legal moves include that square. If you add en passant later, you will need to distinguish "moves" from "attacks" more carefully.
isCheckmate(): Checkmate Detection
Checkmate is the game-ending condition. A player is in checkmate when they are in check AND no legal move can get them out of check. This requires trying every possible move for every piece of the checked color.
Core logic:
- If the color is not in check, it is not checkmate (early exit)
- For every piece of the checked color, try every legal move
- After each trial move, check if the king is still in check
- If no move removes the check, it is checkmate
function isCheckmate(color):
if not isInCheck(color): return false
for each piece in board.getAllPieces(color):
for each target in piece.getLegalMoves(board):
captured = board.movePiece(piece.position, target)
stillInCheck = isInCheck(color)
board.undoMove(piece.position, target, captured)
if not stillInCheck: return false // found an escape
return true // no escape exists
This brute-force approach tries every possible move for the checked player. It is O(P * M) trial moves, each costing O(E * M') for the isInCheck scan. For chess, this is at most ~500 trials, which completes in microseconds. Do not optimize until the interviewer asks you to.
Design alternative for checkmate detection: Instead of trying all moves, you could check three specific escape methods: (1) move the king, (2) capture the checking piece, (3) block the check with another piece. This is algorithmically smarter but significantly harder to implement correctly. In an interview, go with the brute-force approach and mention the optimization verbally.
Interview tip: separation of concerns
Notice how cleanly the three methods divide work. makeMove() handles turn management. isInCheck() handles threat detection. isCheckmate() handles endgame detection. Each method is independently testable. If the interviewer asks you to add stalemate, you add one method: isStalemate() checks "not in check AND no legal moves." Nothing else changes.
Full Implementation
The CodeExplorer below contains 12 files organized by responsibility. Read them in this order: value types first (Position, Color, GameState), then the Piece hierarchy (Piece abstract class, then each subclass), then Board, and finally Game. This bottom-up order mirrors how you would explain it on a whiteboard: "Let me start with the building blocks and work up to the orchestrator."
Key things to notice in the code:
- Position is a record (value semantics, immutable)
- Color has an
opponent()method, eliminating scattered ternary operators - Piece has a protected
slide()helper that three subclasses reuse - Pawn is the most complex piece despite being the "simplest" chess piece
- Board.movePiece() returns the captured piece for undo support
- Game.makeMove() follows the exact pseudocode structure from above
// Immutable value object for board coordinates.
// Using a record gives us equals(), hashCode(), toString() for free.
public record Position(int row, int col) {
public boolean isValid() {
return row >= 0 && row < 8 && col >= 0 && col < 8;
}
}
Architecture note
The file structure follows a clean separation: model/ contains all domain objects (value objects, enums, and the piece hierarchy), while service/ contains the Game orchestrator. Board lives in model/ because it is a data structure, not business logic. Game lives in service/ because it enforces rules. This separation means you can test piece movement without a Game, and test Game logic with a pre-configured Board.
Step 5: Trace a Scenario
Let's walk through Scenario 1 from our examples: White opens with pawn e2 to e4. This traces the full path through our class hierarchy and proves the design works end-to-end.
In an interview, pick the simplest happy-path scenario for your first trace. Save edge cases for a second trace if time permits. The trace below follows every method call in order, showing how Game coordinates Board and Pawn without any class needing to know about the others' internals.
The sequence diagram shows the full request lifecycle for a single move. Notice how the flow stays linear: Client talks only to Game, Game talks to Board and Piece, and the response flows back up. No class skips a layer.
Step-by-step breakdown:
Clientcallsgame.makeMove(new Position(1, 4), new Position(3, 4))(e2 to e4 in 0-indexed coordinates)Gameverifies the game is still ACTIVEGameasksBoardfor the piece at e2 and gets the White PawnGameconfirms the Pawn belongs to the current player (WHITE)Gamecallspawn.getLegalMoves(board). The Pawn checks one square forward (e3, empty) and two squares forward (e4, empty, allowed because the pawn is on its starting row). Returns[e3, e4].Gameconfirms e4 is in the legal moves listGamecallsboard.movePiece(e2, e4). Board moves the pawn, returnsnull(no captured piece).GamecallsisInCheck(WHITE)to verify the move did not expose the White king. Board scans all Black pieces; none can reach e1. Not in check.GamecallsisCheckmate(BLACK)to check if Black is mated. Black is not in check, so checkmate is immediately false.GameswitchescurrentTurnto BLACK and returnstrue.
Notice how the entire flow was handled by just three classes (Game, Board, Pawn). The Client never interacts with Board or Pawn directly. Game is the single entry point, which keeps the API surface small and prevents callers from corrupting internal state.
Why trace matters in interviews
Walking through a concrete scenario proves your design works. It also reveals edge cases you might have missed. When I practice LLD problems, I always trace at least two scenarios: one happy path and one error path. Here, the happy path shows the full pipeline. Try tracing Scenario 3 (pinned bishop) yourself to see the undo path in action.
Extensibility
If time allows, interviewers add follow-up twists to test whether the design can evolve without ripping through existing classes. The goal: show that your design has natural extension points. A strong answer names which classes change and which stay untouched.
For each extension below, notice the pattern: only 1-2 classes need changes, and the rest of the system is unaffected. This stability is the payoff from good separation of concerns. In an interview, explicitly name the classes that do NOT change. It demonstrates that you understand the value of your design.
1. "How would you add castling?"
Castling requires coordinating two pieces (King and Rook) in a single move. The King also cannot castle through check or while in check. This is the most complex chess extension because it touches multiple conditions.
Classes that change: King (new hasMoved flag, new moves in getLegalMoves()), Rook (new hasMoved flag), Board (new castle() method or enhanced movePiece()).
Classes that stay the same: Game, all other piece types, Position, Color, GameState.
I would add a hasMoved boolean flag to King and Rook. In King.getLegalMoves(), check if the king has not moved, the target rook has not moved, and the squares between them are empty and not attacked. If all conditions hold, add the castling destination to the king's legal moves. The Board.movePiece() method would need a special case (or a castle() method) to move both pieces. No other classes change.
// In King.getLegalMoves(), after standard one-step moves:
if (!hasMoved) {
// Check kingside castling conditions
// Check queenside castling conditions
// Add castling targets to moves list
}
Interview tip
When discussing castling, mention that the hasMoved flag is also needed for the "king has not passed through check" condition. This shows you understand the depth of the rule, even though you are not implementing it.
2. "How would you add undo/move history?"
Move history is one of the most common follow-ups because it tests whether your design captures enough information to reverse operations. The key insight: our Board already has undoMove(), so the infrastructure is half-built.
I would introduce a Move record that captures from, to, movedPiece, capturedPiece, and any special flags (castling, promotion). Game maintains a Stack<Move> as the move history. Each call to makeMove pushes a Move onto the stack. An undoLastMove() method pops the stack and calls board.undoMove() with the stored data. This is useful not just for player-facing undo, but also for implementing replay functionality and move notation (algebraic notation).
This is essentially the Command pattern: each Move is a command object that knows how to execute and reverse itself. The existing undoMove() on Board already supports reversal, so the change is minimal.
record Move(Position from, Position to, Piece moved, Piece captured)
class Game {
Stack<Move> history = new Stack<>();
void undoLastMove() {
Move last = history.pop();
board.undoMove(last.from(), last.to(), last.captured());
currentTurn = currentTurn.opponent();
}
}
3. "How would you add a computer opponent?"
I would create a MoveStrategy interface with a single method: chooseMove(Board board, Color color): Move. Then implement concrete strategies like RandomMoveStrategy (picks a random legal move) and MinimaxStrategy (evaluates positions using a heuristic). The Game class accepts an optional MoveStrategy for each color. When it is a computer player's turn, Game calls strategy.chooseMove() instead of waiting for external input.
This is the Strategy pattern. The key insight: Game does not change. It still calls makeMove() with a from/to position. The only difference is where those positions come from (human input vs. strategy computation).
interface MoveStrategy {
Move chooseMove(Board board, Color color);
}
class RandomMoveStrategy implements MoveStrategy {
Move chooseMove(Board board, Color color) {
List<Piece> pieces = board.getAllPieces(color);
// pick random piece, pick random legal move
}
}
The beauty of this approach: you can test the AI strategy independently of Game by passing it a Board in any configuration. Feed it a near-checkmate position and verify it finds the winning move. This testability is a direct benefit of the Strategy pattern.
Classes that change: Game (accepts optional MoveStrategy per color), new MoveStrategy interface and implementations.
Classes that stay the same: Board, all Piece subclasses, Position, Color, GameState. The entire domain model is untouched. This is the strongest proof point for your design: a major feature addition that touches zero existing model classes.
Common Interview Mistakes
These are the mistakes I see most often in mock interviews. Each one loses 5-10 minutes of interview time and signals a gap in design thinking. Study this table, and before your interview, make sure you can explain why each "what to do instead" column entry is better.
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Putting all logic in the Game class | Game becomes a God class with hundreds of lines. Move validation, board management, and game rules are all tangled together. | Separate concerns: Piece owns movement, Board owns the grid, Game owns the rules. |
| Starting with code before identifying entities | You build the wrong abstractions and spend time refactoring mid-interview. | Spend 5 minutes listing entities and their responsibilities. Draw the class diagram first. |
| Making Board know about game rules (check, turns) | Board becomes coupled to chess-specific rules. You cannot reuse it for other board games or test it independently. | Board is a dumb grid. Game is the rule enforcer. Board should not import GameState or know about turns. |
| Using a single Piece class with a type field | Every method needs if (type == ROOK) ... else if (type == KNIGHT) ... checks. Adding a piece type means editing every method. | Use inheritance. Each piece subclass owns its own getLegalMoves(). |
| Forgetting to undo moves after self-check detection | The board is corrupted. Any subsequent move operates on wrong state. A single forgotten undo can make the game unplayable. | Always pair movePiece with undoMove on the failure path. Test this explicitly. |
| Over-engineering with every design pattern | Observer for move events, Singleton for Board, Factory for pieces, Memento for undo: the interviewer asked for a chess game, not a pattern catalog. | Use patterns only when they solve a real problem. Inheritance for pieces and Template Method for sliding are enough. |
The #1 time sink
The most time-consuming mistake is starting to code before finishing the class diagram. When you code first, you discover missing relationships mid-implementation and have to backtrack. Spend the first 8 minutes on entities, relationships, and the class diagram. The remaining 27 minutes of a 35-minute LLD round is plenty for pseudocode and Java.
Interview pro tip: When the interviewer says "Design a chess game," the first words out of your mouth should be clarifying questions, not "So I'll start with a Piece class." Resist the urge to code. The best candidates spend the first third of the interview on requirements and design.
Interview pro tip: Draw the class diagram before writing any code. Point at it while explaining relationships. Interviewers evaluate your communication as much as your design.
Interview pro tip: When writing makeMove(), narrate your edge cases as you go. Say "First I check if the game is over, then I verify it is the right player's turn, then I check if the move is in the piece's legal moves..." This running commentary shows structured thinking and helps the interviewer follow your logic.
Test Your Understanding
Quick Recap
- Start with entities, not code. For chess: Game (rules), Board (grid), Piece (movement), Position (coordinates). Listing entities and responsibilities takes 2 minutes and prevents you from building wrong abstractions.
- Use inheritance when multiple types share an interface but differ in behavior. Each chess piece has a unique
getLegalMoves()implementation, but they all share color, position, and the method signature. - Extract shared logic with Template Method. Rook, Bishop, and Queen all slide along directions. The
slide()helper on the abstract Piece class eliminates the duplication. - Separate coordinators from data managers. Game enforces rules. Board manages the grid. Pieces know their own moves. No class does two of these jobs. This is SRP in action.
- Validate moves against self-check using move-then-verify. Execute the move, check if the king is attacked, undo if it is. This is simpler than predicting the outcome before moving.
- Design for extensibility by keeping each class focused. Adding castling changes King and Board. Adding undo adds a Move record and a stack. Adding AI adds a strategy interface. None of these changes ripple through the entire codebase.
- Write pseudocode before Java. In an interview, the interviewer wants to see your thinking process for
makeMove()andisInCheck(). Pseudocode forces you to articulate the algorithm clearly. Java syntax comes last, and it should be a direct translation of your pseudocode.
Final thought
Chess is one of the best LLD interview problems because it tests inheritance, polymorphism, separation of concerns, and state management all in one question. Master this design, and you will be well-prepared for any turn-based game system: checkers, Battleship, card games, or custom board games. The entity identification and pattern selection methodology transfers directly.