Design Tic-Tac-Toe (Low-Level Design / OOD)
Design Tic-Tac-Toe in an LLD interview with extensibility in mind: Game/Board/Player/Piece classes, O(N) win-checking with running row/column/diagonal counts, and designing for N×N and other grid games.
🟢 Low-Level Design · Case Study · Fresher-friendly → 🟠senior
"Design Tic-Tac-Toe" is a friendly LLD warm-up, but the real test is whether you design it to be extensible — to an N×N board, or even a different game like Connect-4 — rather than hardcoding a 3×3 grid. It's a nice showcase of clean OOD on a small, familiar problem. Let's design it well.
Step 1 — Requirements
- Functional: two players alternate placing marks on an N×N board; detect a win (full row / column / diagonal) or a draw (board full); reject invalid moves.
- Non-functional: extensible to any N (and ideally other grid games), efficient win-checking.
Step 2 — The classes
- Game — orchestrates the flow: whose turn, apply move, check result.
- Board — an N×N grid of cells; knows how to place a piece and report its state.
- Player — has a name and a Piece/Symbol (X or O).
- Piece / Symbol — the mark; making it a type (not a raw char) allows more players/symbols.
- Position / Move — a (row, col).
- Enum
GameStatus{IN_PROGRESS, WIN, DRAW}.
Step 3 — Efficient win-checking
🟠The naive check after every move scans the whole board (O(N²)). A cleaner, O(N)-per-move trick: keep running counters — a count per row, per column, and the two diagonals, incremented (+1 for X, −1 for O) as pieces are placed. After a move at (r,c), if |rowCount[r]| == N or |colCount[c]| == N or a diagonal count hits ±N, that player has won. You only touch the affected row/col/diagonal, and you never rescan the board.
boolean placeAndCheck(int r, int c, int player) { // player = +1 (X) or -1 (O)
board[r][c] = player;
rows[r] += player; cols[c] += player;
if (r == c) diag += player;
if (r + c == n - 1) antiDiag += player;
int win = n; // need n in a line
return Math.abs(rows[r])==win || Math.abs(cols[c])==win
|| Math.abs(diag)==win || Math.abs(antiDiag)==win;
}
Step 4 — Designing for extensibility
🟠The senior signal is not hardcoding 3: parameterise the board by N and the win-length. Represent symbols as Piece objects so you could add players or symbols. Keeping win-logic in the Board (or a pluggable WinningStrategy) means you could swap in Connect-4 rules (first to 4 in a line, gravity) without rewriting the Game. The Game just manages turns and asks the Board "is this a winning/valid move?" — clean separation of responsibilities.
Step 5 — Turn & status management
🟠The Game holds a turn pointer (alternating players, or a queue for >2 players), validates each move (in-bounds, cell empty), applies it, then updates GameStatus (WIN if the check passes, DRAW if the board fills with no winner, else IN_PROGRESS). Using a queue of players makes it trivially extensible beyond two.
The interview-ready walkthrough
"I'd model a Game orchestrating Players (each with a Piece) placing on an N×N Board, with a GameStatus enum. I'd not hardcode 3 — parameterise board size and win-length for extensibility. For win-checking I keep running counts per row, column, and diagonal (+1/−1 per symbol) and after each move check only the affected line, so it's O(N) not O(N²). The Game validates and alternates turns (a player queue extends to N players), and I'd keep win-rules pluggable so Connect-4 could reuse the structure." That balances a clean model with the efficiency and extensibility they're probing for."
What to read next
- Design Chess — the same ideas on a much richer board game
- Design Snake & Ladder — another board-game LLD
- OOD Basics — the responsibilities/relationships toolkit
- ← The complete System Design guide (hub)