OOD Fundamentals
OOP Foundations
SOLID Principles
Creational Patterns
Structural Patterns
Behavioral Patterns
Classic OOD Problems: Part 1
Design a Chess Game
Chess is the gold standard of OOD interview problems. It exercises polymorphism at its purest: six piece types that share an interface but behave completely differently. Before writing any code, you need to nail down what the system does and what it does not do.
Core Use Cases
The system supports a complete two-player chess game:
- Start a new game: initialize the board with 32 pieces in standard starting positions. White moves first.
- Make a move: a player selects a piece and a destination square. The system validates the move and updates the board.
- Detect check: after every move, determine if the opponent's king is under attack.
- Detect checkmate and stalemate: if a player has no legal moves, the game ends. Checkmate (king is in check) is a loss. Stalemate (king is not in check) is a draw.
- Handle special moves: castling, en passant, and pawn promotion follow additional rules beyond basic piece movement.
- Track move history: record every move for undo functionality and algebraic notation display.
Chess is not about building a chess engine or AI. Interviewers want to see how you model entities, use polymorphism for piece behavior, and handle complex validation rules. Focus on the OOD: clean class hierarchy, single responsibility, and extensibility.
What We Defer
We explicitly exclude: AI opponents, network multiplayer, timers, tournament management, and GUI rendering. These are real features but they expand scope beyond a 45-minute design session. The system is a local two-player game with a programmatic interface.
Core Entities
The key objects fall into natural groups:
- Game: orchestrates the match. Tracks whose turn it is, manages game state (active, check, checkmate, stalemate, resigned), and coordinates between players and the board.
- Board: 8x8 grid of squares. Handles piece placement, removal, and lookup by position.
- Square: a single cell on the board. Holds coordinates (row, column) and optionally a piece.
- Piece: abstract base class with concrete subtypes: King, Queen, Rook, Bishop, Knight, Pawn. Each implements its own movement logic.
- Player: represents a human player with a color (white or black) and a collection of captured pieces.
- Move: encapsulates a move: source square, destination square, piece moved, piece captured (if any), and special move type.
- MoveHistory: ordered list of moves with undo capability.
The relationships are straightforward: a Game has two Players, one Board, and one MoveHistory. The Board has 64 Squares arranged in an 8x8 grid. Each Square holds at most one Piece. Each Piece belongs to one Player (via color).
The board and piece hierarchy are the structural foundation of the system. Get this right and everything else, move validation, check detection, special moves, falls into place naturally.
Board Representation
The Board is an 8x8 grid of Square objects. Each Square stores its row (0-7) and column (0-7) and holds an optional reference to a Piece.
The Board does not know chess rules. It provides spatial operations: place a piece, remove a piece, get the piece at a position, check if a position is within bounds. The Board is the data structure. The Game and MoveValidator are the algorithms.

The Piece Hierarchy
Every piece in chess shares three properties: a color (white or black), a position (row, column), and a method to compute its valid moves. But each piece type computes valid moves differently. This is textbook polymorphism.
The has_moved flag is critical for three rules: castling (king and rook must not have moved), pawn double advance (only from starting position), and en passant detection.
Implementing Each Piece Type
Rook: moves in straight lines along rows and columns. Slides until hitting the board edge, a friendly piece (blocked), or an opponent piece (can capture, then stop).
Bishop: identical sliding logic but with diagonal directions: (1,1), (1,-1), (-1,1), (-1,-1).
Queen: combines Rook and Bishop directions. Eight directions total. This is why the Queen is the most powerful piece: it has the most movement freedom.
Knight: the only piece that jumps. It moves in an L-shape: two squares in one direction, one square perpendicular. It ignores all pieces in between.
King: moves one square in any direction (8 possibilities). Additionally handles castling (covered in the special moves section).
Pawn: the most complex piece despite being the weakest. It moves forward one square, optionally two from starting position, captures diagonally, and promotes upon reaching the last rank. Direction depends on color (white moves up, black moves down).
In interviews, implement Rook or Knight first to demonstrate the pattern, then say 'Bishop and Queen follow the same sliding logic with different direction vectors.' Do not waste time implementing all six pieces in full: show the pattern and move on to the interesting parts like check detection.
Why Sliding Logic is Shared
Rook, Bishop, and Queen all use the same algorithm: pick a direction, advance one square at a time, stop when hitting a boundary or piece. The only difference is the direction vectors. You can extract this into a helper method on Piece:
Then Rook calls self.get_sliding_moves(board, [(0,1),(0,-1),(1,0),(-1,0)]) and Bishop calls self.get_sliding_moves(board, [(1,1),(1,-1),(-1,1),(-1,-1)]). Queen calls it with all eight directions. This eliminates code duplication while keeping each piece class focused on its own unique behavior.
A move is not valid just because a piece can physically reach a destination. Chess has layered validation rules, and your design must enforce all of them cleanly.
The Validation Pipeline
Every move attempt passes through four checks, in order:
- Piece ownership: the piece belongs to the current player (correct color and it is their turn).
- Piece reachability: the destination is in the piece's
get_valid_moves()result. - Path clearance: for sliding pieces (Rook, Bishop, Queen), no friendly or opponent piece blocks the path. The Knight skips this check entirely because it jumps.
- King safety: after making the move on a temporary board, the player's own king must not be in check. This is the most subtle rule: you cannot make a move that exposes your own king to capture.

Steps 2 and 3 are already handled inside each piece's get_valid_moves(): the sliding logic only returns squares the piece can actually reach. Step 4 is the one that requires additional work.
The King Safety Check
This is the single most important validation rule in chess. A player can never leave their own king in check. This means:
- You cannot move your king into an attacked square.
- You cannot move a piece that is currently blocking an attack on your king (a "pinned" piece).
- If your king is in check, your move must resolve the check (move the king, block the attack, or capture the attacker).
The implementation simulates the move on a copy of the board, then checks whether any opponent piece can reach the king's square:
Why Board.copy() Matters
The king safety check must simulate the move without modifying the actual board. If you mutate the real board to test king safety, you have to undo the mutation afterward: which introduces bugs if the undo logic has errors. A defensive copy is safer: copy the board, apply the move on the copy, check if the king is safe, discard the copy.
The cost is copying 64 squares and up to 32 piece references per validation. This is negligible for a turn-based game: even a brute-force AI evaluating thousands of moves per second can afford shallow copies.
The most common bug in chess implementations is forgetting the king safety check. A move that looks valid for the piece may expose the king to capture. Every single move, including captures and special moves, must pass the king safety simulation before being accepted.
Separating Validation from Execution
The MoveValidator is a stateless utility. It does not modify the board. The Game class calls the validator before executing any move:
This separation means you can unit test validation independently of game state. You can also reuse the validator for move suggestion features, puzzle solving, or AI move generation.
Chess has three special moves that break the normal movement rules. Each one adds complexity to your design, and interviewers love asking about them because they test how well your class hierarchy handles exceptions to the rules.
Castling
Castling is the only move where two pieces move simultaneously. The king moves two squares toward a rook, and the rook jumps to the other side of the king.

Preconditions (all must be true):
- The King has never moved (
has_moved == False) - The Rook has never moved (
has_moved == False) - No pieces between the King and Rook
- The King is not currently in check
- The King does not pass through any square that is under attack
- The King does not land on a square that is under attack
This is where the has_moved flag on Piece pays off. Without it, you would need to scan the entire move history to determine if the King or Rook has moved: an O(n) operation for every castling check.
En Passant
En passant is a pawn capture that can only occur on the move immediately after an opponent's pawn advances two squares. Your pawn captures the opponent's pawn "in passing" by moving diagonally to the square the opponent's pawn skipped.
Preconditions:
- Your pawn is on the 5th rank (row 3 for white, row 4 for black)
- An adjacent opponent pawn just advanced two squares (on the immediately preceding move)
- Your pawn captures by moving diagonally to the square behind the opponent's pawn
The "just advanced" condition requires access to the last move in the MoveHistory. This is why MoveHistory is a first-class object: en passant validation needs it.
Pawn Promotion
When a pawn reaches the opposite end of the board (8th rank for white, 1st rank for black), it must be promoted to another piece: usually a Queen, but sometimes a Knight, Rook, or Bishop (underpromotion).
The Move class needs a promotion_piece field to specify what the pawn becomes. The Board removes the pawn and places the promoted piece in the same square.
The Command Pattern for Moves
Special moves demonstrate why the Command pattern is essential. Each Move object encapsulates everything needed to execute and undo the action:
For undo, the Move stores the captured piece so it can be restored, the castling flag so the rook can be moved back, and the promotion piece so the pawn can be re-created. Without this information, undo is impossible without replaying the entire game from the start.
Game State Machine
The Game tracks its state through a simple state machine:
- ACTIVE: the game is in progress, players alternate turns.
- CHECK: the current player's king is in check. They must resolve it on their next move.
- CHECKMATE: the current player is in check and has no legal moves. The opponent wins.
- STALEMATE: the current player is not in check but has no legal moves. The game is a draw.
- RESIGNED: a player has resigned. The opponent wins.
After every move, the Game updates its state by checking if the opponent is now in check and whether they have any legal moves.
The endgame logic is where your design comes together. Check and checkmate detection uses every component you have built: piece movement, board queries, move validation, and the king safety simulation.
Detecting Check
A king is in check if any opponent piece includes the king's square in its valid moves. The implementation iterates all opponent pieces and calls get_valid_moves() on each:
This is an O(p * m) operation where p is the number of opponent pieces (up to 16) and m is the average number of valid moves per piece. For a typical mid-game position, this scans roughly 100-200 squares: trivial for modern hardware.

Detecting Checkmate
Checkmate occurs when a player is in check AND has no legal move that escapes the check. There are exactly three ways to escape check:
- Move the King to a square that is not under attack.
- Block the attack by placing a piece between the attacker and the King (only works against sliding pieces: not Knights or Pawns).
- Capture the attacker with any of your pieces.
Rather than implementing these three cases separately, use a brute-force approach that is both simpler and more correct: try every legal move for every piece of the checked player. If any move results in the king no longer being in check, it is not checkmate.
Detecting Stalemate
Stalemate is identical to checkmate detection except the king is NOT in check. The player has no legal moves but is not under attack. This is a draw, not a loss.
Notice that has_any_legal_move() is shared between checkmate and stalemate detection. The only difference is the initial check condition. This is a sign of good decomposition: the complex logic (enumerating all legal moves) is written once and reused.
Updating Game State After Each Move
After every move, the Game checks the opponent's situation:
The order matters: checkmate is checked before stalemate because checkmate requires the king to be in check. If you check stalemate first, a checkmate position (no legal moves + king in check) would incorrectly be classified as stalemate.
A common bug is checking stalemate before checkmate. Both conditions share 'no legal moves available,' but checkmate additionally requires the king to be in check. Always check checkmate first, then stalemate. Getting this order wrong turns losses into draws.
Additional Draw Conditions
Beyond stalemate, chess recognizes several other draw conditions that a complete implementation should handle:
- Threefold repetition: the same board position occurs three times (requires position hashing in MoveHistory).
- Fifty-move rule: 50 consecutive moves by each player with no capture and no pawn move.
- Insufficient material: neither player has enough pieces to checkmate (e.g., King vs King, King+Bishop vs King).
These are tracked through MoveHistory and piece counting: further justifying why MoveHistory is a rich object rather than a simple list of strings.
Practice
Apply everything you have learned about chess OOD. These problems test piece polymorphism, move validation, and game state detection.
Loading problem...
Loading problem...
Loading problem...