Course Content
Object-Oriented Design Interview
14 sections · 29 lessons
Tic Tac Toe: requirements and where responsibilities belong
Tic tac toe is the smallest problem in the course, and that is what makes it a trap. With no domain and no scale to hide behind, the whole score comes down to where responsibilities are placed. This lesson covers the prompt, the requirements, and the two placement decisions the problem is really about.
Why a trivial game is a real interview question
Tic tac toe is given to junior candidates constantly, and the reason is that it removes every excuse. There is no domain to learn, no ambiguity about the rules, and no scale. What is left is: can you place responsibilities correctly, and can you make one small optimisation cleanly?
That makes it dangerous in a specific way. Candidates relax, write a Game class with a 2D array and a hundred-line play() method, and produce something that works perfectly and demonstrates nothing. A small problem still deserves a real design — that is the lesson, and interviewers use this problem precisely because it separates people who model out of habit from people who model only when forced.
The four questions that change the model
1. "Fixed 3×3, or N×N with K in a row?" This is the question that matters most. A 3×3 board can be won by checking eight lines of three — you can hard-code it and nobody will care. An N×N board with K in a row makes the win check an actual algorithm, and it is where the interesting code in Win detection, in code comes from. Ask, and propose N×N even if they say 3×3: "I'll design for N×N with K in a row so the win check generalises — it costs nothing extra."
2. "Two human players, or a computer opponent?" A computer opponent introduces the Strategy pattern (The seven patterns that actually appear) and is the natural extension. Assume two humans for the core, with the interface shaped so a bot slots in.
3. "Is undo needed?" Undo is the requirement that justifies keeping a move history, and without it a move list is speculative. Ask before building one.
4. "Does the game need to be replayable or recorded?" Replay means the move log is the source of truth and the board is derived from it — a genuinely different emphasis. Assume not, but mention it.
Assumptions this lesson makes
- An N×N board with K in a row to win; the defaults are N=3, K=3.
- Two players alternating turns, each with a distinct piece.
- Moves are validated: inside the board, on an empty cell, and by the player whose turn it is.
- The game ends on a win or when the board is full (a draw).
- Single-threaded, one game per instance. Concurrency is not part of this problem, and saying so is better than inventing it.
Requirements
Functional requirements
1. Start a game with two players and a board of size N, needing K in a row2. Accept a move: a player, a row, and a column3. Validate the move: in bounds, cell empty, and the correct player's turn4. Detect a win after each move5. Detect a draw when the board is full with no winner6. Report the game's outcome and stop accepting movesRequirement 3 has three separate checks in it, and a design that handles them in three different places is one of the ways this problem goes wrong. Finding the objects, below, puts them where they belong.
Non-functional requirements
N1. Win detection should not rescan the whole board after every move. On a 3×3 board this is meaningless — 24 reads is nothing. On a 15×15 Gomoku board with K=5, a full scan per move is about 900 cell reads while the incremental check is about 4. This requirement exists to give the round something to optimise, and the optimisation is genuinely satisfying, which is why Win detection, in code exists.
N2. Adding a new player type (a bot) must not modify the game loop.
N3. The rules must be data, not branches. N and K are configuration; a design that hard-codes 3 in eight places cannot become Connect Four.
What is cut
CUT (named, not forgotten)- Networking, matchmaking, and lobbies- Persistence between sessions- A user interface — the design exposes a method, not a screen- Scoring across multiple games, tournaments, ratingsSay: "I'll expose makeMove(player, row, col) and let a console or a web layer call it. The interface is the boundary; I'm not designing a screen."
The scale sentence
There is no scale here, and saying so is better than pretending:
"There's no performance story in a 3×3 game — everything is instant. The only place efficiency becomes real is if we generalise to a large board, which is why I want to write the win check as if N were large. Otherwise I'd be optimising nothing."
That is honest and it sets up Win detection, in code. Inventing a scale problem where none exists is a different failure mode, and interviewers notice it.
Finding the objects
The candidate nouns
Game, board, cell, player, move, piece, symbol, turn, winner, result, position.
Filtering them
| Candidate | Verdict | Reason |
|---|---|---|
Game | Class | Owns turn order and the game's status |
Board | Class | Owns the grid and, as argued below, the win check |
Cell | Borderline | A cell holds one nullable piece and nothing else |
Player | Class | A name and a piece; gains behaviour when bots arrive |
Move | Class | Needed for undo and replay; a small value object |
Piece | Enum | X, O — a closed set with no behaviour |
Turn, Winner | Not classes | State on Game, not entities |
On Cell. A separate Cell class holding one Piece field is defensible and mostly ceremony. A Piece[][] where null means empty is honest and shorter. Pick either, and say why: "I'll use a Piece[][] with null for empty rather than a Cell class — a cell has no behaviour of its own. If cells gained behaviour, like being blocked or scoring differently, I'd promote it to a class." Naming the condition under which you would change your mind is the part that scores.
The two placement decisions this problem is about
Decision one: Player must not hold the board.
A common first attempt gives Player a makeMove() method that reaches into the board:
1// The version to reject2public class Player {3 private Board board;4 public void makeMove(int row, int col) {5 board.place(row, col, piece); // the player mutates shared state directly6 }7}Three problems. Every player holds a reference to shared mutable state, so nothing controls turn order — two players can both move. Validation has no single home. And when a bot arrives, it holds a board it can mutate at will rather than being asked for a decision.
The correct shape: a player decides, the game applies.
1public class Player {2 private final String name;3 private final Piece piece;4 // A human player is asked for a move by the interface layer.5 // A bot implements a strategy that returns a move given a read-only board view.6}Game.makeMove(player, row, col) is the only path that mutates anything. Turn order, validity, and outcome are all checked in one place.
Decision two: the win check belongs to Board, not Game.
The rule from Phase 4: assigning responsibilities is put behaviour with the data it needs. Win detection reads the grid and nothing else — no turn order, no player names, no game status. Put it on Game and Game must either read the board's internals (breaking encapsulation) or the board must expose its grid, which is the same thing with more steps.
Put it on Board and the incremental counters in Win detection, in code live beside the grid they summarise — which is what makes that optimisation possible at all.
One sentence per class
Game — enforces turn order and reports the game's outcome.Board — holds the grid and answers whether the last move won.Player — identifies one participant and their piece.Move — records one placement: player, row, column, and sequence number.Piece — which mark occupies a cell.Five, all passing the one-sentence test, for a problem most people solve with one class and an array.