Tic-Tac-Toe
Prefer to learn one decision at a time?
Try questions, reveal the reasoning, trace the code, and finish with the simulator.
Design a two-player game that accepts legal moves, alternates turns, and stops with a win or draw.

1. Requirements (~5 minutes)
The interview begins with one sentence:
“Design Tic-Tac-Toe.”
You probably know how Tic-Tac-Toe works. The danger is assuming that your version of the rules is the interviewer's version. The sentence does not tell us the board size, who starts, how invalid moves are reported, or whether features such as undo are required.
Before thinking about classes, turn the familiar game into a precise promise that our code can keep.
Use these four question groups every time
What must a user be able to do?
When does an action succeed, fail, or change state?
Which invalid actions must we reject?
What should we deliberately not build?
Ask these questions
AskHow many players are there, and how are their marks assigned?Show answerHide answer
Interviewer answer
There are two local players. One uses X and the other uses O. X starts.
AskIs the board always 3 by 3? What exactly counts as a win?Show answerHide answer
Interviewer answer
Use a fixed 3 by 3 board. Three equal marks in a row, column, or diagonal wins.
AskWhen is the game a draw, and can moves continue after the game ends?Show answerHide answer
Interviewer answer
It is a draw when all nine cells are filled without a winner. Reject every move after a win or draw.
AskWhich moves are invalid, and how should the caller learn why?Show answerHide answer
Interviewer answer
Reject a wrong player, a cell outside the board, an occupied cell, or a move after completion. Return a clear result.
AskDo we need a computer player, undo, score history, larger boards, or online play?Show answerHide answer
Interviewer answer
No. Discuss them only if asked as extensions.
Confirm the specification
Confirmed specification
6 requirements · 6 boundaries
The first version must
- 1.Two local players use X and O; X takes the first turn.
- 2.Players alternate placing one mark on an empty cell of a fixed 3 by 3 board.
- 3.A row, column, or diagonal containing three equal marks wins.
- 4.A full board without a winner is a draw.
- 5.Reject the wrong player, out-of-range cells, occupied cells, and moves after completion.
- 6.Expose the board, current player, status, winner, and a reset operation.
Not building in this version6 boundaries
- Computer opponent
- Undo or replay history
- Score across matches
- Variable board size
- Online multiplayer
- UI and storage
2. Entities and relationships (~3 minutes)
Now return to the confirmed requirements and look for candidate objects: game, player, board, cell, mark, and winning rule. We will not keep all of them as classes.
For each candidate, ask: what information changes here, and which rule needs that information? The class should be placed where the answer is clearest.
Final model
Types we will create
- Class
- Owns changing state or coordinates rules.
- Record
- A Java record for validated, immutable data with value equality.
- Interface
- Defines behavior that another implementation can replace.
Coordinates turns, match status, winner, and reset.
Owns cells, placement checks, and winning lines.
Carries one player's validated name and fixed mark.
Enums
Enums name a small, fixed set of valid choices or outcomes.
Limits a cell to X, O, or empty.
Names whether the match is active, won, or drawn.
Explains why a move succeeded or was rejected.
Fields
Fields are simple values stored inside another type; they do not need independent behavior in this version.
Stores the current Mark grid inside Board.
Identifies the selected horizontal position.
Identifies the selected vertical position.
See one move flow through the objects
Swipe to follow the flow →

Read the arrows in order. A player sends makeMove to Game. Game first checks the current player and match status. It then asks Board to place the mark. Board checks the coordinate and cell, stores the mark only when valid, and returns accepted or rejected. Game changes the turn only after an accepted placement. That is why the UI talks to Game instead of editing Board directly.
3. Class design (10–15 minutes)
The entity step told us which objects we need. Class design turns each object into a clear promise: the state it protects, the behavior it owns, and the small public API other objects may use.
Use the same path for every class:
- Start with one responsibility from the requirements.
- Add only the state needed to perform that responsibility.
- Put behavior beside the state it reads or changes.
- Expose a public method only when another object or caller needs it.
- Check relationships and protect the rules that must never be bypassed.
State is information an object remembers. Behavior is a decision or change it can perform using that information. An invariant is a rule that must remain true—for example, a cell cannot contain two marks and turns cannot change after a rejected move.
One class at a time
Design the state owners
Start with the coordinator, then follow the request into the classes that protect changing state. Select a class to see only the fields and behavior needed for its responsibility.
Class 1 of 2
Game
Coordinates one match without duplicating Board's grid rules.
Requirement behind this class
Players alternate, moves stop after completion, the winner and status are readable, and reset starts a clean match.
State it remembers
Private information required to perform this class's job.
Composes the grid owner used by this match.
Keeps the two fixed participants.
Remembers whose move is allowed.
Records the winner when the match completes.
Rejects moves after a win or draw.
Behavior it owns
Public use cases and internal rules kept beside that state.
Validates match flow, delegates placement, and resolves the result.
Clears Board and restores the initial turn and status.
Expose required state without returning mutable Board.
Update turn, winner, and status only after an accepted move.
- Invariant it protects
- Only an in-progress match accepts a move, and rejected moves change no match state.
- Works with
- Two Player records and one Board.
- Does not own
- Cell storage, coordinate checks, placement, or winning-line calculations.
- Principle demonstrated
- Single responsibility and composition keep match flow separate from grid rules.
Supporting types
These types carry stable data or define replaceable behavior. They support the state owners without becoming another place to store live workflow state.
Carries a validated name and fixed Mark.
Its compact constructor rejects incomplete identity; generated accessors expose stable data with value equality.
Decide whether a pattern helps
Before choosing a pattern, ask what rule has multiple versions right now. The answer is none: there is one fixed board, one winning rule, and two human players. Direct composition is easier to explain and test.
Do not force: Strategy for the basic game
The requirements contain one fixed winning rule and two human players. A WinningStrategy or MoveStrategy interface would create extra code without any second implementation. Write the direct rule first.
Do not force: Singleton for Game
We may want several games in tests or in a future tournament. A normal object supports that naturally. Global singleton state would make this harder.
Do not force: Factory and Observer
Constructors are enough for one simple setup, and the confirmed scope has no UI subscribers. Add factories or observers only when creation paths or notifications actually vary.
Use: Strategy when several computer players are added
If an extension requires easy, medium, and hard computer players, those are several ways to choose a move. At that point a MoveStrategy interface has a real purpose. Do not add it before that requirement arrives.
Review the final class blueprint
The final model is deliberately small. Every field pays for a requirement, every rule sits beside the state it needs, and callers cannot mutate the active board without passing through Game.makeMove.
Use the tabs to inspect each class on its own. The arrows and field types show how the three small responsibilities combine without exposing Board's mutable grid.

How to read it: Player keeps a name and one fixed Mark together as stable identity.
Design principle: Immutability and cohesion prevent a participant's identity changing during a match.
4. Implementation (~10 minutes)
This is the complete Java version. The main method changes state only after validation succeeds.
What the code does
It creates two players, stores marks on a three-by-three board, accepts or rejects moves, alternates turns, and ends the match with a win or draw.
Why it is structured this way
Board owns cell and winning-line rules. Game owns the match workflow: whose turn it is, whether play may continue, and who won. Keeping these jobs separate makes invalid-state changes easier to prevent.
Follow one request through the code
- 1A caller sends Game.makeMove with a player, row, and column.
- 2Game checks match status and turn order before touching the board.
- 3Board validates the cell and places the mark only when it is empty.
- 4Game checks win, then draw, otherwise changes the current player.
Code concepts to understand
Validate first, change state secondA rejected move must leave the board and current player exactly as they were.
Game.makeMove checks completion and turn order first. Board.place then checks the coordinates and cell. Only after placement succeeds does Game calculate the result or switch players.
This order is important. If the turn changed before the board accepted the move, clicking an occupied cell would incorrectly give the other player a turn.
Board rules versus game workflowBoard answers questions about cells; Game controls the match.
Board knows whether a coordinate is valid, whether a cell is empty, whether a mark has a winning line, and whether the board is full. It does not know player names or whose turn it is.
Game knows the current player, status, and winner. It calls Board instead of reading or changing the cell array directly. This gives both classes one clear reason to change.
Enums instead of strings and booleansMark, GameStatus, and MoveResult give every meaningful state a clear name.
An enum prevents invalid values such as "FINSHED" and avoids combinations of booleans such as isWon=true and isDraw=true. MoveResult also tells the caller why a move failed without throwing an exception for an expected user action.
Why no Strategy pattern in the basic gameThere is only one fixed human move flow and one fixed winning rule.
A pattern should solve a current source of change. The basic requirements do not include bots, several board sizes, or selectable winning rules, so extra interfaces would make the first design harder to read.
If the interviewer adds computer players, introduce MoveStrategy. If they add different winning rules, introduce WinningRule. The extension creates the need for the pattern.
Enums
These enums define the complete vocabulary used by the rest of the program. They remove magic strings and make method results readable at the call site.
public enum Mark {
X,
O
}public enum GameStatus {
IN_PROGRESS,
X_WON,
O_WON,
DRAW
}public enum MoveResult {
ACCEPTED,
NOT_YOUR_TURN,
OUT_OF_BOUNDS,
CELL_OCCUPIED,
GAME_ALREADY_OVER
}Player and board
Player is stable identity data: a name and one fixed mark. Board owns the mutable cell array and all rules that can be answered by looking only at that array. Notice that callers cannot receive and modify the array directly.
import java.util.Objects;
public record Player(String name, Mark mark) {
public Player {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name is required");
}
mark = Objects.requireNonNull(mark);
}
@Override
public String toString() {
return name + " (" + mark + ")";
}
}import java.util.Arrays;
public final class Board {
public static final int SIZE = 3;
private final Mark[][] cells = new Mark[SIZE][SIZE];
public boolean place(int row, int column, Mark mark) {
if (!isInside(row, column) || mark == null) return false;
if (cells[row][column] != null) return false;
cells[row][column] = mark;
return true;
}
public boolean isInside(int row, int column) {
return row >= 0 && row < SIZE && column >= 0 && column < SIZE;
}
public boolean hasWinningLine(Mark mark) {
for (int index = 0; index < SIZE; index++) {
if (cells[index][0] == mark
&& cells[index][1] == mark
&& cells[index][2] == mark) {
return true;
}
if (cells[0][index] == mark
&& cells[1][index] == mark
&& cells[2][index] == mark) {
return true;
}
}
boolean mainDiagonal = cells[0][0] == mark
&& cells[1][1] == mark
&& cells[2][2] == mark;
boolean otherDiagonal = cells[0][2] == mark
&& cells[1][1] == mark
&& cells[2][0] == mark;
return mainDiagonal || otherDiagonal;
}
public boolean isFull() {
for (Mark[] row : cells) {
for (Mark cell : row) {
if (cell == null) return false;
}
}
return true;
}
public Mark getMark(int row, int column) {
if (!isInside(row, column)) {
throw new IndexOutOfBoundsException("Cell is outside the board");
}
return cells[row][column];
}
public void clear() {
for (Mark[] row : cells) {
Arrays.fill(row, null);
}
}
@Override
public String toString() {
StringBuilder output = new StringBuilder();
for (int row = 0; row < SIZE; row++) {
for (int column = 0; column < SIZE; column++) {
Mark mark = cells[row][column];
output.append(mark == null ? "." : mark);
if (column < SIZE - 1) output.append(" ");
}
output.append(System.lineSeparator());
}
return output.toString();
}
}Game controller
Game.makeMove is the central workflow and should be read from top to bottom: reject illegal game state, reject the wrong player, validate the cell, place the mark, calculate the result, and switch the turn only if play continues. TicTacToeDemo proves how the public API is meant to be used.
import java.util.Objects;
public final class Game {
private final Board board;
private final Player playerX;
private final Player playerO;
private Player currentPlayer;
private Player winner;
private GameStatus status;
public Game(Player first, Player second) {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
if (first.mark() == second.mark()) {
throw new IllegalArgumentException("Players need different marks");
}
this.playerX = first.mark() == Mark.X ? first : second;
this.playerO = first.mark() == Mark.O ? first : second;
this.board = new Board();
reset();
}
public MoveResult makeMove(Player player, int row, int column) {
if (status != GameStatus.IN_PROGRESS) {
return MoveResult.GAME_ALREADY_OVER;
}
if (player != currentPlayer) {
return MoveResult.NOT_YOUR_TURN;
}
if (!board.isInside(row, column)) {
return MoveResult.OUT_OF_BOUNDS;
}
if (!board.place(row, column, player.mark())) {
return MoveResult.CELL_OCCUPIED;
}
if (board.hasWinningLine(player.mark())) {
winner = player;
status = player.mark() == Mark.X
? GameStatus.X_WON
: GameStatus.O_WON;
} else if (board.isFull()) {
status = GameStatus.DRAW;
} else {
currentPlayer = currentPlayer == playerX ? playerO : playerX;
}
return MoveResult.ACCEPTED;
}
public void reset() {
board.clear();
currentPlayer = playerX;
winner = null;
status = GameStatus.IN_PROGRESS;
}
public Mark getMark(int row, int column) {
return board.getMark(row, column);
}
public Player getCurrentPlayer() {
return currentPlayer;
}
public Player getWinner() {
return winner;
}
public GameStatus getStatus() {
return status;
}
}public final class TicTacToeDemo {
public static void main(String[] args) {
Player alice = new Player("Alice", Mark.X);
Player bob = new Player("Bob", Mark.O);
Game game = new Game(alice, bob);
play(game, alice, 0, 0);
play(game, bob, 1, 0);
play(game, alice, 0, 1);
play(game, bob, 1, 1);
play(game, alice, 0, 2);
printBoard(game);
System.out.println("Status: " + game.getStatus());
System.out.println("Winner: " + game.getWinner());
}
private static void play(Game game, Player player, int row, int column) {
MoveResult result = game.makeMove(player, row, column);
System.out.printf("%s -> (%d,%d): %s%n",
player.name(), row, column, result);
}
private static void printBoard(Game game) {
for (int row = 0; row < Board.SIZE; row++) {
for (int column = 0; column < Board.SIZE; column++) {
Mark mark = game.getMark(row, column);
System.out.print((mark == null ? "." : mark)
+ (column == Board.SIZE - 1 ? "" : " "));
}
System.out.println();
}
}
}Verify the design with a real scenario
- 1Initial: the board is empty, status is IN_PROGRESS, and Alice (X) is current.
- 2Alice plays (0,0). Board accepts X and Game changes current player to Bob.
- 3Alice tries another move immediately. Game returns NOT_YOUR_TURN and the board does not change.
- 4Bob plays (1,0), then turns continue until Alice completes row 0.
- 5After Alice plays (0,2), Board finds X X X, Game sets X_WON, stores Alice as winner, and does not switch turns.
- 6Any later move returns GAME_ALREADY_OVER and the winning board stays unchanged.
Try the flow below. Click the same cell twice to confirm that a rejected move does not switch the player.
Try it yourself
Play and inspect each move
Choose an empty cell. Try clicking an occupied cell too.
Live object state
Game.status
IN_PROGRESS
Game.current
Player X
Board.moves
0 / 9
What happened
- Game created. Player X owns the first turn.
5. Extensions (~5 minutes)
“Add computer players with several difficulty levels”
Now introduce a MoveStrategy interface with chooseMove(Board). Easy, medium, and hard bots implement different selection rules. Human players still receive moves from input, so do not put minimax logic inside Board or Game.
“Support an N by N board and K marks in a row”
Pass size into Board and move win checking behind a WinningRule interface. The fixed eight-line implementation can then be replaced without changing the turn workflow in Game.
“Add undo”
Store each accepted move in a stack. undo() removes the most recent mark, restores the previous player, and changes status back to IN_PROGRESS. Rejected moves never enter the history.
“Play over a network”
Keep this domain model. Add a service around Game that identifies the remote player, serializes commands, and ensures that two moves cannot update one game at the same time.