LLD for Tic-Tac-Toe: Designing an Extensible OOP Game
Deep Dive: Why the Deque Is the Right Structure for Turn Management
Tic-Tac-Toe looks trivial β until the interviewer says "make it NΓN with P players and pluggable winning rules." The key design decisions: a Board abstracted from piece identity, a Strategy Pattern for win conditions, and a Factory for player creation.
π― System Scenario & Requirements
System Goal
Tic-Tac-Toe looks trivial β until the interviewer says "make it NΓN with P players and pluggable winning rules." The key design decisions: a Board abstracted from piece identity, a Strategy Pattern for win conditions, and a Factory for player creation.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Tic-Tac-Toe looks trivial β until the interviewer says "make it NΓN with P players and pluggable winning rules." The key design decisions: a Board abstracted from piece identity, a Strategy Pattern for win conditiβ¦
- Getting these right is the interview objective.
- A fixed two-player array breaks the moment a third player is added.
Constraints & Assumptions
- Strategy Pattern for win conditions: Passing WinningStrategy into Game via constructor injection means adding a new win rule is a new class β zero changes to Game .
β‘ Execution Stages & Decision Trace
Stage 1 β The Basics: Object-Oriented Design for Games
A common interview mistake is designing Tic-Tac-Toe as a 3Γ3 hardcoded grid.
The interviewer immediately asks: "What if I want a 10Γ10 board?
Deep Dive
Scenario context
Tic-Tac-Toe looks trivial β until the interviewer says "make it NΓN with P players and pluggable winning rules." The key design decisions: a Board abstracted from piece identity, a Strategy Pattern for win conditions, and a Factory for player creation. Getting these right is the interview objective.
A fixed two-player array breaks the moment a third player is added. A Deque<Player> implements round-robin rotβ¦
This walkthrough focuses on Deep Dive: Why the Deque Is the Right Structure for Turn Management.
Before making a design choice, clarify:
- A fixed two-player array breaks the moment a third player is added.
- A Deque<Player implements round-robin rotation with two operations: pollFirst() takes the current player and offerLast() returns them after their turn.
- This scales to N players without index arithmetic.
Concepts in play: low-level-design-systems, design-principles-solid, stack-and-queue-data-structures, jvm-internals-and-garbage-collection
Stage 1 β The Basics: Object-Oriented Design for Games
- A common interview mistake is designing Tic-Tac-Toe as a 3Γ3 hardcoded grid.
- The interviewer immediately asks: "What if I want a 10Γ10 board?
- Or 4-in-a-row instead of 3?" The design that handles this is what we're building.
- Low-level design interviews test whether you can decompose a system into clean, extensible objects.
Observable outcome: Low-level design interviews test whether you can decompose a system into clean, extensible objects.
Stage 2 β From 3Γ3 Toy to OOP Interview Test
- Every LLD interview question hides the same trap: the naive solution is obvious, so the interviewer immediately extends it.
- Naive version: a fixed 3Γ3 grid, two hardcoded players, if-else win check.
- Extended version: NΓN board, P players with distinct symbols, swappable winning strategies (row, column, diagonal, custom), and clean state transitions.
- This post builds the extensible version step by step.
Observable outcome: This post builds the extensible version step by step.
Stage 3 β Core Entities β Board, Piece, Player, Game
- A clean OOP model needs exactly four entities: Enums and value objects first:
classDiagram
class PlayingPiece {
+PieceType type
}
class Player {
+String name
+PlayingPiece piece
}
class Board {
+int size
+PlayingPiece[][] grid
+placePiece(row, col, piece)
+isFull()
}
class Game {
+Board board
+Deque players
+WinningStrategy strategy
+startGame()
+checkWinner(row, col, piece)
}
Player --> PlayingPiece
Game --> Board
Game --> Player
Game --> WinningStrategy
enum PieceType { X, O, STAR, HASH } // extensible β add symbols without changing Board
class PlayingPiece {
final PieceType type;
PlayingPiece(PieceType t) { this.type = t; }
}
class Player {
final String name;
final PlayingPiece piece;
Player(String name, PlayingPiece piece) {
this.name = name;
this.piece = piece;
}
}
Observable outcome: A clean OOP model needs exactly four entities: Enums and value objects first:
Stage 4 β Extending to NΓN: Board with Variable Size
- The Board must not hardcode 3.
- Size is a constructor parameter; the grid is a 2D array of nullable PlayingPiece .
- Multiple player support β use a Deque for round-robin turns, regardless of player count: Each turn: poll() from front, play, then offerLast() to back.
class Board {
final int size;
private final PlayingPiece[][] grid;
Board(int size) {
this.size = size;
this.grid = new PlayingPiece[size][size];
}
public boolean placePiece(int row, int col, PlayingPiece piece) {
if (row < 0 || row >= size || col < 0 || col >= size) return false;
if (grid[row][col] != null) return false; // cell occupied
grid[row][col] = piece;
return true;
}
public PlayingPiece getPiece(int row, int col) { return grid[row][col]; }
public boolean isFull() {
for (PlayingPiece[] row : grid)
for (PlayingPiece p : row)
if (p == null) return false;
return true;
}
}
Deque<Player> players = new LinkedList<>();
players.add(new Player("Alice", new PlayingPiece(PieceType.X)));
players.add(new Player("Bob", new PlayingPiece(PieceType.O)));
players.add(new Player("Carol", new PlayingPiece(PieceType.STAR)));
Observable outcome: Multiple player support β use a Deque for round-robin turns, regardless of player count: Each turn: poll() from front, play, then offerLast() to back.
Stage 5 β OOP Pillars Applied in the Game Design
- Each of the four OOP pillars maps directly onto a specific class or interface in this design β not as a textbook exercise, but as a concrete decision that makes the code extensible.
- Encapsulation in practice: Board 's constructor allocates grid as a private field.
- The only public surface is placePiece(row, col, piece) , getPiece(row, col) , and isFull() .
- If the internal representation changes from a 2D array to a Map<Position, Piece , no external class breaks β callers never touch grid[row][col] directly.
classDiagram
direction LR
class Player {
<>
+Piece piece
+makeMove(Board) Move*
}
class HumanPlayer {
+makeMove(Board) Move
}
class AiPlayer {
+makeMove(Board) Move
}
class WinStrategy {
<>
+checkWin(Board, Piece) bool
}
class RowColWinStrategy
class DiagonalWinStrategy
HumanPlayer --|> Player : extends
AiPlayer --|> Player : extends
RowColWinStrategy ..|> WinStrategy : implements
DiagonalWinStrategy ..|> WinStrategy : implements
Game --> Player : uses
Game --> WinStrategy : depends on
Observable outcome: If the internal representation changes from a 2D array to a Map<Position, Piece , no external class breaks β callers never touch grid[row][col] directly.
Stage 6 β SOLID Principles in the Tic-Tac-Toe Design
- Every SOLID principle surfaces in a specific, traceable design decision here β not as abstract advice: OCP in practice: the product team wants a "diagonal-only" win rule for a new game variant.
- You write DiagonalOnlyWinStrategy implements WinStrategy .
- Existing Game , Board , and RowColWinStrategy are untouched.
- The open/closed boundary is the WinStrategy interface.
// Game depends on the abstraction β never on a concrete class
class Game {
private final WinStrategy strategy;
Game(Board board, List<Player> players, WinStrategy strategy) {
this.strategy = strategy; // injected β any WinStrategy works here
}
}
// The call site decides which concrete strategy is used β Game never knows
Game standard = new Game(board, players, new RowColWinStrategy());
Game variant = new Game(board, players, new DiagonalOnlyWinStrategy());
Observable outcome: The open/closed boundary is the WinStrategy interface.
End-to-end outcome
A fixed two-player array breaks the moment a third player is added.
Alternatives considered
Decision Guide: When to Apply This OOP Pattern
Decision Guide: When to Apply This OOP Pattern
Failure and degraded modes
Trade-offs & Failure Modes: Trade-offs and Design Decisions
- Detection: Strategy Pattern for win conditions: Passing WinningStrategy into Game via constructor injection means adding a new win rule is a new class β zero changes to Game .
- User or operator impact: Strategy Pattern for win conditions: Passing WinningStrategy into Game via constructor injection means adding a new win rule is a new class β zero changes to Game .
