AlgoMaster Logo

Design Tic Tac Toe Game

Last Updated: January 22, 2026

Ashish

Ashish Pratap Singh

easy
10 min read

In this chapter, we will explore the low-level design of a tic tac toe game in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting any design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope.

Here is an example of how a discussion between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • The game is played on a 3x3 grid.
  • Two players take alternate turns, identified by markers ‘X’ and ‘O’.
  • The game should detect and announce the winner.
  • The game should declare a draw if all cells are filled and no player has won.
  • The game should reject invalid moves and inform the player.
  • The system should maintain a scoreboard across multiple games.
  • Moves can be hardcoded in a driver/demo class to simulate gameplay.

1.2 Non-Functional Requirements

  • The design should follow object-oriented principles with clear responsibilities and separation of concerns.
  • The system should be modular and extensible to support future features like larger boards, AI opponent, move history, etc.
  • The game logic should be testable and easy to maintain.
  • The system should provide clear console output that reflects the current state of the game board.

After the requirements are clear, the next step is to identify the core entities that we will form the foundation of our design.

2. Identifying Core Entities

How do you go from a list of requirements to actual entities/classes? The key is to look for nouns in the requirements that have distinct attributes or behaviors. Not every noun becomes a class, but this approach gives you a starting point.

Let's walk through our requirements and identify what needs to exist in our system.

1. The game is played on a 3x3 grid.

The grid is central to everything. We need something to represent it. This gives us our first entity: Board.

But what is a grid made of? Individual squares. Each square can be empty or contain a symbol. This suggests a second entity: Cell. The Board will contain 9 Cells arranged in a 3x3 structure.

2. Two players take alternate turns, identified by markers ‘X’ and ‘O’.

We need to represent players. Each player has a name and an assigned symbol. This gives us the Player entity.

What about the symbols themselves? We could use strings ("X", "O") or characters, but that's error-prone. What stops someone from creating a player with symbol "Z"?

Using an enum Symbol with values XO, and EMPTY gives us type safety. The compiler will catch invalid symbols at compile time rather than runtime.

3. The game processes moves and determines game outcomes.

Something needs to coordinate the gameplay: accept moves, validate them, check for wins, switch turns. This orchestrator is our Game entity.

The game also needs to track its current state. Is it still in progress? Did someone win? Is it a draw?

We could use a boolean isGameOver and a winner field, but that gets messy. What if we need to distinguish between "X won" and "O won"?

An enum GameStatus with values IN_PROGRESSWINNER_XWINNER_O, and DRAW captures all possibilities cleanly.

4. The system should maintain a scoreboard across multiple games.

A single Game object handles one game. But our requirements say we need to track scores across multiple games. This suggests two more entities:

  • Scoreboard: Tracks how many times each player has won
  • TicTacToeSystem: A central controller that creates games and maintains the shared scoreboard

The TicTacToeSystem acts as a facade. External code doesn't need to know about Game, Board, or Scoreboard directly. It just calls system.createGame() and system.makeMove().

Entity Overview

Here's how these entities relate to each other:

We've identified three types of entities:

Enums define fixed sets of values. They provide type safety and make code self-documenting.

Data Classes primarily hold data with minimal behavior. Player and Cell are simple containers.

Core Classes contain the main logic. Board manages the grid, Game orchestrates gameplay, Scoreboard tracks history, and TicTacToeSystem ties everything together.

Scroll
EntityTypeResponsibility
SymbolEnumCell values: X, O, or EMPTY
GameStatusEnumGame state: IN_PROGRESS, WINNER_X, WINNER_O, DRAW
CellData ClassHolds a single symbol
PlayerData ClassHolds player name and assigned symbol
BoardCore ClassManages the 3x3 grid
GameCore ClassOrchestrates gameplay and win detection
ScoreboardCore ClassTracks wins across games
TicTacToeSystemCore ClassFacade for the entire system

With our entities identified, let's define their attributes, behaviors, and relationships.

3. Designing Classes and Relationships

Now that we know what entities we need, let's flesh out their details. For each class, we'll define what data it holds (attributes) and what it can do (methods). Then we'll look at how these classes connect to each other.

3.1 Class Definitions

We'll work bottom-up: simple types first, then data containers, then the classes with real logic. This order makes sense because complex classes depend on simpler ones.

Enums

Enums define fixed sets of values that provide type safety and make code self-documenting. Using enums prevents invalid states at compile time rather than runtime.

Symbol

Represents the values a cell can contain.

Scroll
ValueDisplay CharacterPurpose
X'X'First player's marker
O'O'Second player's marker
EMPTY'_'Unoccupied cell

Each enum value maps to a display character for printing the board. 

Why not just use characters directly?

Because Symbol.X is self-documenting and type-safe. You can't accidentally pass 'Z' where a symbol is expected. The compiler catches invalid symbols at compile time, not runtime.

GameStatus

Defines the possible states of the game. Tracks where we are in the game lifecycle.

Scroll
ValueDescriptionTerminal?
IN_PROGRESSGame is still being playedNo
WINNER_XPlayer with X symbol wonYes
WINNER_OPlayer with O symbol wonYes
DRAWBoard is full, no winnerYes

Four distinct states cover all possible game outcomes. A game starts as IN_PROGRESS and transitions to exactly one terminal state when it ends.

Data Classes

Data classes are simple containers that hold data with minimal behavior. They represent the "nouns" in our system that have attributes but little logic.

Player

Holds player information.

Scroll
AttributeTypeDescription
nameStringPlayer identifier (e.g., "Alice")
symbolSymbolThe marker assigned to the player (X or O)
MethodDescription
Player(name, symbol)Constructor with validation (rejects EMPTY symbol)

The Player class is immutable. Once created, a player's name and symbol don't change. This prevents bugs where someone accidentally reassigns a player's symbol mid-game.

Cell

Holds the current value of a board position.

Scroll
AttributeTypeDescription
symbolSymbolCurrent value: X, O, or EMPTY
MethodDescription
Cell()Constructor, initializes symbol to EMPTY
isEmpty()Returns true if symbol is EMPTY

Unlike Player, Cell is mutable. It starts as EMPTY and gets set to X or O when a player makes a move. The isEmpty() helper method makes calling code more readable: if (cell.isEmpty()) is clearer than if (cell.getSymbol() == Symbol.EMPTY).

Core Classes

Core classes contain the actual game logic. They coordinate between data classes and implement the rules of the game.

Board

Encapsulates the 3x3 grid and handles all board-related operations including its state and the rules for checking win/draw conditions.

Scroll
AttributeTypeDescription
gridCell[][]2D array of cells
sizeintBoard dimension (3 for standard game)
MethodDescription
Board(size)Constructor, creates size×size grid of empty cells
placeSymbol(row, col, symbol)Places a symbol at the given position
isCellEmpty(row, col)Returns true if the cell is available
isFull()Returns true if no empty cells remain
printBoard()Displays the current board state to console

The Board doesn't know about players or game rules. It just manages a grid of cells. This separation means we could reuse Board for other grid-based games.

Game

The orchestrator that brings all components together and manages gameplay.

Scroll
AttributeTypeDescription
boardBoardThe game board
playersPlayer[]The two players
currentPlayerIndexintWhose turn it is (0 or 1)
statusGameStatusCurrent game state
winningStrategiesList<WinningStrategy>Strategies for win detection
observersList<GameObserver>Listeners for game end events
MethodDescription
Game(p1, p2, boardSize)Constructor, initializes all components
makeMove(row, col)Core method: validate, place, check win/draw, switch turn
addObserver(observer)Register a listener for game end events
notifyObservers()Notify all listeners that game ended

Game ties everything together. It owns the Board, knows the Players, tracks whose turn it is, and uses WinningStrategies to detect wins. When the game ends, it notifies observers (like the Scoreboard).

Scoreboard

Tracks wins across multiple games.

Scroll
AttributeTypeDescription
scoresMap<String, Integer>Maps player names to win counts
MethodDescription
recordWin(player)Increment a player's win count
getScore(playerName)Get a player's current score
printScoreboard()Display all scores

The Scoreboard implements GameObserver so it can automatically update when games end.

TicTacToeSystem

This is the public-facing facade.

Scroll
AttributeTypeDescription
instanceTicTacToeSystemSingleton instance
scoreboardScoreboardShared scoreboard
currentGameGameThe active game
MethodDescription
getInstance()Get the singleton instance
createGame(player1, player2)Start a new game
makeMove(player, row, col)Make a move in the current game
printScoreboard()Display scores

External code only interacts with TicTacToeSystem. It doesn't need to know about Board, Cell, or WinningStrategy.

3.2 Class Relationships

How do these classes connect? There are three types of relationships we use.

Composition (Strong Ownership)

Composition means one object owns another. When the owner is destroyed, the owned object is destroyed too.

  • Board owns Cells: When you create a Board, it creates 9 Cells. Those Cells don't exist outside the Board. When the Board is garbage collected, so are its Cells.
  • Game owns Board: Each Game creates its own Board. The Board exists only for that game.

Association (Weak Reference)

Association means one object uses another, but doesn't own it. Both objects have independent lifecycles.

  • Game uses Players: The Game receives Player objects but doesn't create them. The same Player can participate in multiple games. If a Game ends, the Player objects continue to exist.
  • Game uses WinningStrategies: The Game uses strategies to check for wins, but the strategies could be shared across games.
  • TicTacToeSystem uses Scoreboard: The system references a Scoreboard but the Scoreboard has its own lifecycle.

Implementation (Interface Contract)

Implementation means a class fulfills an interface contract.

  • RowWinningStrategy, ColumnWinningStrategy, DiagonalWinningStrategy implement WinningStrategy: All three classes can check for wins, but each checks a different pattern.
  • Scoreboard implements GameObserver: Scoreboard receives notifications when games end, but Game doesn't know it's talking to a Scoreboard specifically.

3.3 Key Design Patterns

You might notice some structural patterns emerging in our design. Let's make them explicit and justify why each pattern is appropriate here.

Strategy Pattern (Win Detection)

The Problem: A player can win in three distinct ways: completing a row, a column, or a diagonal. If we hardcode all win conditions in a single method, we end up with a long, complex function that's hard to test and modify. Adding a new win condition (like "four corners" in a variant) would require changing existing code.

The Solution: The Strategy pattern encapsulates each win-checking algorithm in its own class. The Game holds a list of WinningStrategy implementations and iterates through them to check for a winner.

Why Strategy Pattern?

We could use a simple switch statement or a series of if-else blocks. However, the Strategy pattern gives us:

  • Testability: Each strategy can be unit tested in isolation
  • Extensibility: Adding new win conditions means adding a new class, not modifying existing code
  • Single Responsibility: Each strategy handles exactly one type of win check

Observer Pattern (Scoreboard Updates)

The Problem: When a game ends, the Scoreboard needs to update. The naive approach is to have the Game directly call scoreboard.recordWin(). But this couples the Game to the Scoreboard. What if we want to add analytics tracking? Or a replay recorder? Each new listener would require modifying the Game class.

The Solution: The Observer pattern decouples the Game (subject) from its listeners (observers). The Game maintains a list of observers and notifies them when the game ends. Observers implement a common interface.

Why Observer Pattern? 

For a single Scoreboard, direct method calls would work fine. We use Observer because:

  • It demonstrates proper decoupling (important for interviews)
  • It makes adding new listeners trivial (analytics, logging, replays)
  • It keeps the Game focused on game logic, not notification logistics

Singleton Pattern (TicTacToeSystem)

The Problem: We need a single, globally accessible entry point to the system that maintains a consistent scoreboard across multiple games.

The Solution: The Singleton pattern ensures only one instance of TicTacToeSystem exists. It provides a global access point via getInstance().

Why Singleton Pattern?

Singleton is often overused, but it's appropriate here because:

  • We genuinely need one scoreboard shared across all games
  • The system acts as a facade for the entire application
  • It simplifies the client code (no need to pass system references around)

3.4 Full Class Diagram

Now that we've designed our classes and relationships, let's bring this to life with code.

Try It Yourself (Exercise)

Before looking at the complete implementation, try building the Tic-Tac-Toe system yourself. Below you'll find template stubs for all the classes we designed. Each stub includes method signatures and // TODO comments where you need to add the implementation logic.

Your task: Implement all the // TODO sections based on the design we discussed. Once complete, run the demo to verify your implementation produces the expected output.

Loading editor...

4. Code Implementation

Now let's translate our design into working code. We'll build bottom-up: foundational types first, then data classes, then the classes with real logic. This order matters because each layer depends on the ones below it.

4.1 Enums

We start with the two enums that other classes depend on.

Each Symbol maps to a display character. This keeps display logic centralized. If we later want to use 'x' instead of 'X', we change it in one place.

Four possible states. The game starts IN_PROGRESS and ends in one of the three terminal states.

4.2 Custom Exception

Before we write classes that can fail, let's define how they fail. A custom exception makes error handling cleaner than catching generic RuntimeException.

We'll throw this when someone tries to play on an occupied cell, make a move after the game ends, or specify an out-of-bounds position.

4.3 Data Classes

These are simple containers. They hold data with minimal logic.

Notice the constructor validation. A player with Symbol.EMPTY makes no sense, so we reject it immediately. This is "fail fast" design. If you create an invalid Player, you find out right away, not three hours later when debugging a weird game state.

Both fields are final. Once you create a Player, their name and symbol never change. Immutability prevents bugs.

Unlike Player, Cell is mutable. It starts empty and gets filled during gameplay. The isEmpty() helper makes calling code more readable: if (cell.isEmpty()) is clearer than if (cell.getSymbol() == Symbol.EMPTY).

4.4 Interfaces

Now we define the contracts that our strategy and observer classes will implement.

The interface takes the board, the position of the last move, and the symbol to check. Each implementation decides how to use these parameters. Row strategy only cares about row. Column strategy only cares about col. Diagonal strategy ignores both and checks the whole diagonal.

Simple notification interface. When a game ends, observers get the Game object and can extract whatever information they need (winner, final board state, etc.).

4.5 Strategy Implementations

Each strategy checks one way to win. Let's implement all three.

RowWinningStrategy checks if all cells in the row of the last move contain the same symbol.

We iterate through every column in the given row. If any cell doesn't match, return false immediately. No need to check further.

ColumnWinningStrategy works the same way, but iterates through rows instead of columns.

DiagonalWinningStrategy is more complex because there are two diagonals: main (top-left to bottom-right) and anti-diagonal (top-right to bottom-left).

The main diagonal has cells at positions (0,0), (1,1), (2,2). The anti-diagonal has cells at (0,2), (1,1), (2,0). Notice how size - 1 - i gives us the anti-diagonal column index.

Each strategy is independently testable. You can unit test RowWinningStrategy without creating a full Game. Just create a Board, set up a winning row, and verify the strategy returns true.

4.6 Board Class

The Board encapsulates all grid operations. It doesn't know about players, turns, or game rules. It just manages a 2D array of cells.

A few things to note about the Board:

  • Constructor creates all cells: The initializeBoard() method runs in the constructor, so you never have a Board with null cells.
  • Validation is centralized: The validatePosition() method is private and called by every public method that takes coordinates. This prevents code duplication.
  • isFull() short-circuits: As soon as we find an empty cell, we return false. No need to scan the entire board.
  • printBoard() is for debugging: In a real application, you'd probably have a separate view layer. But for interviews and testing, a simple print method is useful.

4.7 Game Class

This is where everything comes together. The Game coordinates players, board, strategies, and observers. It's the most complex class, but each method has a single responsibility.

Let's break down the key design decisions in the Game class:

Thread Safety: The makeMove method is synchronized. This prevents two threads from making moves simultaneously, which could corrupt the game state. The observer list uses CopyOnWriteArrayList, which allows safe iteration even if observers are added during notification.

The makeMove flow:

  1. Check if game is over (fail fast)
  2. Validate the cell is empty
  3. Place the symbol
  4. Check for win using all strategies
  5. Check for draw if no winner
  6. Switch to next player if game continues

Strategy iteration: The checkWin method iterates through all strategies. As soon as one returns true, we have a winner. This is where the Strategy pattern pays off. Adding a new win condition just means adding another strategy to the list.

Observer notification: We only notify observers when the game ends (win or draw). This keeps the observer interface simple. If we needed move-by-move notifications, we could add a separate onMove() method to GameObserver.

4.8 Scoreboard Class

The Scoreboard demonstrates the Observer pattern in action. It listens for game end events and automatically updates scores.

The Scoreboard is decoupled from the Game. It doesn't know when games start or how moves work. It just receives a notification when a game ends, extracts the winner, and updates its internal map.

Note the use of ConcurrentHashMap and the merge() method. The merge() call atomically gets the current value (or 0 if absent), adds 1, and stores the result. This is thread-safe without explicit synchronization.

4.9 TicTacToeSystem (Singleton Facade)

The system class is the public entry point. External code only needs to know about this class. It hides the complexity of Game, Board, and Scoreboard behind a simple interface.

Singleton Implementation Details:

  • Double-checked locking: We check instance == null twice. The first check avoids the cost of synchronization when the instance already exists. The second check (inside the synchronized block) handles the race condition where two threads both pass the first check.
  • volatile keyword: This ensures that when one thread creates the instance, other threads immediately see the fully constructed object. Without volatile, threads might see a partially constructed instance due to instruction reordering.
  • resetInstance() for testing: Singletons are notoriously hard to test because the instance persists across tests. This method lets us reset the singleton between tests. In production, you'd probably remove this or make it package-private.

Facade Benefits:

The TicTacToeSystem class simplifies the API. Compare these two approaches:

Without facade:

With facade:

The facade handles object creation, wiring, and lifecycle. Callers don't need to know that games have observers or that scoreboards exist.

4.10 Demo Class

Let's see the system in action with a demo that plays three games.

Move Sequence Diagram

The following diagram illustrates what happens when a player makes a move:

5. Run and Test

Loading editor...

6. Quiz

Design Tic Tac Toe Quiz

1 / 21
Multiple Choice

Which entity is primarily responsible for managing the state of the Tic Tac Toe grid?