Skip to main content
Articles/design patterns/lld-for-tic-tac-toe-designing-a-scalable-game
Series: Object-Oriented Design Exercisesβ€’Part 5 of 5
Scenario Article

LLD for Tic-Tac-Toe: Designing an Extensible OOP Game

Deep Dive: Why the Deque Is the Right Structure for Turn Management

Abstract Algorithmsβ€’Feb 20, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1 β€” The Basics: Object-Oriented Design for Games

Action / Decision:

A common interview mistake is designing Tic-Tac-Toe as a 3Γ—3 hardcoded grid.

Component Behavior:

The interviewer immediately asks: "What if I want a 10Γ—10 board?

Observable Output: Or 4-in-a-row instead of 3?" The design that handles this is what we're building.
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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 .

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit