Tic-Tac-Toe is the “hello world” of low-level design interviews — deceptively small, but a great lens for what they’re really testing: can you turn fuzzy requirements into clean, extensible objects without over-engineering?
Before we model anything, play a few rounds. This is the exact system we’re about to design — two players, alternating turns, first to get three in a line wins.
Nail the requirements first
Before a single class, pin down what “done” means. Splitting functional from non-functional is what separates a design that works from one that scales.
- Two players alternate turns
- Place a mark on an empty cell
- Reject moves on taken / out-of-range cells
- Detect a win (row, column, diagonal)
- Detect a draw (board full, no winner)
- Board size configurable (3×3 → N×N)
- Win length K configurable (not always N)
- Win-detection cheap per move
- Rules & players pluggable without touching core
Separate the components
The single most important decision is drawing boundaries. Group by responsibility, and every future change lands in exactly one place.
| Layer | Owns | Classes |
|---|---|---|
| Entities | Value types, identity | Symbol, Position, Player |
| State | The mutable grid | Board |
| Rules | When is the game won? | WinningStrategy (+ implementations) |
| Orchestration | Turn flow, status | TicTacToe (the game controller) |
| I/O | Input & rendering | console / web / the playable board above |
Entities: the vocabulary
Start with the small, dumb value types. They carry no logic — just meaning.
Each class should have one reason to change. is the rule that produced these types — and the whole layering above.
Players — and why they’re a hierarchy
A Player isn’t just a name + a symbol. Making it abstract with a next_move() method is what lets a human and a computer be interchangeable — the game loop never needs to know which is playing.
State: the Board
The Board owns the grid and nothing else. It knows how to place a mark and report its own shape, but it does not know the rules of winning.
Rules: the win check, where design actually matters
The naive approach re-scans every row, column, and both diagonals after each move: O(N²) work per move. Fine for 3×3, wasteful at scale. The elegant move is to track counts incrementally.
- O(N²) work every single move
- Re-reads cells that never changed
- Simple to write, poor to scale
- O(1) per move (amortized O(N))
- Only the affected line is touched
- One counter per row, col, diagonal
Behind an interface so the rule is swappable — this is the Strategy pattern:
Assign X = +1 and O = -1. A line is won only when it’s filled by a single symbol — meaning all N cells agree in sign. Summing them gives +N (all X) or -N (all O); any mix cancels toward zero. So abs(sum) == N is a necessary and sufficient test for that line, and it costs a single addition per move instead of a scan.
This generalizes cleanly: for K-in-a-row on a large board, swap the fixed counters for a short sliding-window scan of length K around the placed cell — still local, still far cheaper than a full rescan.
Orchestration: the game controller
TicTacToe is the conductor. It holds players and turn order, delegates storage to Board and rule-checking to a WinningStrategy, and drives one turn at a time.
The codeable interface
Here’s the whole contract on one screen — the four seams a reader (or an interviewer) can implement against. If you can fill these in, you’ve understood the design:
Wiring it all together is a few lines — note how nothing here knows how a player decides or how winning is computed:
Extensibility, for free
- N×N board → Board(size=n)
- K-in-a-row → new WinningStrategy
- Computer player → new Player subclass
- 3+ players → TicTacToe turn logic only
- Board (storage is size-agnostic)
- Player contract (next_move)
- The game loop (depends on interfaces)
- Entities (Symbol, Position)
Edge cases worth stating out loud
Calling these out unprompted is a strong signal in an interview:
Practice
Reading a design is easy; reproducing it is where it sticks. Try the challenge, then check yourself.
The key idea: only lines through the just-placed cell can change, so scan outward in the four axes and keep the longest run. Write it in your language below — the interfaces are pre-filled — then reveal the reference. Cost is O(k) per move, and TicTacToe never learns this rule exists: that’s the Strategy pattern paying off.
The best LLD answers aren’t the ones with the most classes — they’re the ones where every class has an obvious job and the design bends exactly where the requirements said it would.
Discussion
Sign in to join the conversation.