Design Snake & Ladder
Board, dice, snakes, ladders, and turn management — small surface, plenty of clean-modelling opportunities.
Problem Statement
Design the object model for Snake & Ladder, a turn-based board game where multiple players wait in a queue, roll a dice, move across numbered cells, apply any snake or ladder jump they land on, and win only when they reach the final cell exactly.
The interview focus is not graphics or networking. Focus on the game engine: board setup, dice Strategy, fair turn ordering, movement rules, event publication, and clean seams for custom boards or deterministic tests.
Business context
Snake & Ladder is a beginner-friendly game design question used by Amazon, Flipkart, and Microsoft because the rules are simple but the modelling signals are strong. A good solution separates immutable board layout from mutable player state, keeps dice pluggable through Strategy, centralizes board construction through a Factory Method, and exposes game events through an Observer-style seam rather than scattering print statements through the code.
Functional Requirements
Create an N by N board whose final cell is N squared.
Represent snakes and ladders as jumps from one cell to another cell.
Reject invalid jumps such as a jump from the first cell, a jump from the final cell, duplicate jump starts, or endpoints outside the board.
Support two or more players and process turns in queue order.
Roll dice through a pluggable Dice strategy so tests can use deterministic rolls and production can use random rolls.
Resolve a move by applying the dice roll, staying in place on overshoot, then applying at most one jump from the landing cell.
Declare a winner as soon as a player reaches the final cell exactly and stop re-queuing that player.
Publish move, jump, and win events so a console, UI, logger, or scoreboard can observe the game without owning game rules.
Non-Functional Requirements
Deterministic testability
The dice must be injected so a unit test can replay exact rolls without depending on randomness.
Rule correctness
Overshoot, jump validation, queue rotation, and win detection must be enforced by domain objects rather than by UI code.
Extensible setup
A new board layout should be created by a factory or configuration loader without changing Game.playTurn.
Low latency
A turn should be constant time after setup: roll once, check one jump map entry, update one player, and rotate the queue.
Separation of concerns
The game engine should not know whether events are printed, stored, streamed, or rendered in a UI.
Requirement Clarification
QDoes a player need the exact roll to win?
Yes. If the roll would move beyond the final cell, the player stays at the current position and the turn passes.
QCan a cell have both a snake and a ladder?
No. At most one jump may start from a cell. The Board rejects duplicate jump starts during setup.
QCan jumps chain if the destination also has a jump?
Not in the base design. Apply at most one jump for the landing cell. Chained jumps are a documented variant.
QWhere do dice rolls come from?
From an injected Dice strategy. RandomDice is production friendly; tests can provide a fixed-sequence Dice implementation.
QDo we need persistence or multiplayer networking?
No for the core LLD. Keep the engine in memory and expose events so external adapters can persist or broadcast later.
UML Class Diagram
Sequence Diagram
Entity Identification
Game
Aggregate root for a running match. Owns the Board, the Dice strategy, the player queue, and the winner state.
Board
Owns board dimension and jump lookup. It validates jump placement, calculates overshoot behavior, and resolves landing-cell jumps.
Jump
Immutable value object for a snake or ladder. Direction is derived from start and end rather than stored as a separate flag.
Dice
Strategy interface for dice rolls. Game depends on this abstraction, not on randomness.
RandomDice
Default Dice implementation that returns a uniformly random value from one to the configured number of sides.
Player
Mutable participant state: a validated name and the current board position, starting from cell 1.
Design Patterns Used
Dice is injected into Game, so random production rolls and deterministic test rolls share the same turn engine.
Board setup belongs in a factory method that returns a fully configured Board from a named layout or config. Game should receive a ready Board and never know how snakes and ladders were authored.
Move, jump, and win announcements should be events. The reference code has a single announce seam; production code should notify observers from that seam instead of coupling Game to console, UI, or storage.
Step-by-Step Design
1Start with board invariants
Board owns the dimension, final cell, and jump map. It rejects invalid starts, duplicate jump starts, and endpoints outside the board during setup so Game can trust the layout.
2Model snakes and ladders as one Jump value object
A snake and a ladder differ only by direction. If end is smaller than start it is a snake; if end is larger it is a ladder. This avoids parallel Snake and Ladder classes with duplicated fields.
3Inject dice as a Strategy
Game calls Dice.roll and does not care whether the implementation is RandomDice, a fixed-sequence test dice, or a weighted dice variant.
public interface Dice { int roll(); } public class RandomDice implements Dice { public int roll() { return random.nextInt(sides) + 1; } }4Resolve a turn in one place
Game.playTurn should poll the queue, roll once, ask Board for the target, apply one jump if present, update the player, then either set the winner or offer the player back to the queue tail.
Player player = players.poll(); int before = player.getPosition(); int roll = dice.roll(); int target = board.targetAfterRoll(before, roll); Jump jump = board.getJumpAt(target); int after = jump == null ? target : jump.getEnd(); player.setPosition(after);
5Centralize layout creation in a Factory Method
The core implementation exposes Board.addJump. In an interview, wrap repeated setup in a factory method so named boards, tests, and config-driven layouts do not duplicate construction logic.
public static Board classicBoard() { Board board = new Board(10); board.addJump(new Jump(4, 25)); board.addJump(new Jump(14, 7)); return board; }6Publish events from a single seam
The reference Game uses announce for console messages. Treat that as the Observer seam: notify listeners on move, jump, and win without letting observers mutate Board or Player state.
Complete Java Implementation
Explanation of Every Class
Jump
Immutable value object with start and end cells. It validates positive positions, rejects no-op jumps, derives snake or ladder direction, and provides a human-readable description.
Board
Owns board dimension and a map from jump start cell to Jump. It validates jumps, exposes final cell, handles overshoot by keeping the player in place, and resolves one jump after a roll.
Dice
Small Strategy interface with one roll method. This is the main testability seam because Game depends only on Dice, not on RandomDice.
RandomDice
Default Dice implementation. It validates that the dice has at least two sides and returns a one-based random roll using java.util.Random.
Player
Mutable participant object. It validates the name, starts every player at cell 1, exposes the current position, and guards against non-positive positions.
Game
Coordinates the match. It stores players in a LinkedList-backed Queue, rolls the injected dice, asks Board for target and jump data, updates the current player, announces the move, sets the winner at the final cell, and otherwise rotates the player to the queue tail.
Dry Run
Sample input
10 by 10 board with ladder 4 to 25 and snake 14 to 7. Queue is Asha then Ben. Dice is deterministic for demonstration; the final row is a condensed late-game turn with Asha at 97.
| Turn | Player | Roll | Before | Resolution | Queue after turn |
|---|---|---|---|---|---|
| 1 | Asha | 3 | 1 | lands on 4, ladder moves to 25 | Ben, Asha |
| 2 | Ben | 5 | 1 | moves to 6, no jump | Asha, Ben |
| 3 | Asha | 6 | 25 | moves to 31, no jump | Ben, Asha |
| 4 | Ben | 2 | 6 | moves to 8, no jump | Asha, Ben |
| 5 | Asha | 4 | 31 | moves to 35, no jump | Ben, Asha |
| 6 | Ben | 6 | 8 | lands on 14, snake moves to 7 | Asha, Ben |
| 7 | Asha | 3 | 97 | moves to 100, final cell reached | game ends |
The same turn pipeline handles a ladder, a snake, a normal move, and the winning move. The queue rotates only when the player has not won.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| addJump during setup | O(1) | O(1) | Hash map insert after constant-time validation. |
| playTurn | O(1) | O(1) | Queue poll, dice roll, one jump lookup, one position update, and queue offer. |
| start | O(T) | O(1) | T is the number of turns until a player reaches the final cell. |
| board setup | O(J + P) | O(J + P) | J jumps stored in Board and P players stored in the game queue. |
The hot path is already constant time because the board uses a jump map instead of scanning snakes and ladders. The unbounded part of the game is not per-turn cost; it is the random number of turns before an exact finish.
Extensibility
Deterministic tests
Add a FixedDice or ScriptedDice implementation. Game needs no change because it already depends on the Dice interface.
Config-driven boards
Move setup into BoardFactory.fromConfig so boards can be loaded from JSON, a database, or interview fixtures while preserving Board validation.
Event subscribers
Replace console printing in announce with GameEventObserver callbacks for UI rendering, audit logs, metrics, or replay recording.
Rule variants
Support chained jumps, extra turns on rolling six, or bouncing back from the final cell by adding a MovementRule strategy around Board.move.
Alternative Designs
Separate Snake and Ladder subclasses
Create Snake and Ladder classes instead of one Jump value object.
Tradeoffs
This can make direction explicit, but it duplicates start and end validation and adds polymorphism without meaningful behavior differences in the base problem.
Board owns player positions
Store player positions in Board rather than in Player.
Tradeoffs
It centralizes all state, but Board becomes responsible for both layout and gameplay. Keeping position on Player makes Board reusable for many games.
Recursive jump resolution
Keep following jumps until a cell without a jump is reached.
Tradeoffs
Useful for a variant, but it needs cycle detection and differs from the simpler one-jump rule most interviews expect.
Event bus instead of direct observers
Publish GameEvent objects to a central event bus.
Tradeoffs
Better for large applications with many subscribers, but direct observers are easier to reason about in a focused LLD interview.
Common Mistakes
- ×
Hard-coding RandomDice inside Game, making deterministic tests painful.
- ×
Representing snakes and ladders as two unrelated maps and forgetting to enforce one jump per start cell.
- ×
Moving past the final cell instead of staying in place on overshoot.
- ×
Re-queuing the winning player and continuing the game after a winner is set.
- ×
Letting UI or console code calculate movement rules instead of observing game events.
- ×
Putting board setup directly in Game.playTurn instead of constructing a valid Board before the game starts.
- ×
Applying chained jumps accidentally when the requirement says only the first landing cell jump applies.
Follow-up Interview Questions
QHow would you test a winning path deterministically?
Inject a ScriptedDice with a known roll sequence, create a small board through a factory method, call playTurn repeatedly, and assert winner plus final positions.
QHow do you support an extra turn when a player rolls six?
Extract queue-rotation policy into a TurnRule strategy. After a move, the rule decides whether the same player stays at the front or moves to the tail.
QHow would you persist a game or support replay?
Record move events emitted by the Observer seam: player, before, roll, target, jump, after, and whether the move won. Replaying events reconstructs visible state.
QWhat changes if the board is not 10 by 10?
Nothing in Game. Board already derives final cell from dimension squared and validates jumps against that final cell.
QHow would you prevent invalid board configs from reaching production?
Keep validation in Board.addJump, run BoardFactory validation at startup, and fail fast if a config has duplicate starts or out-of-range endpoints.
Production Considerations
Configuration validation
Validate board files at startup and expose clear errors for duplicate jump starts, invalid endpoints, or impossible dimensions.
Observability
Emit move and win events with game id, player id, roll, before, target, jump, and after fields so debugging and replay are straightforward.
Fair randomness
Use a well-scoped random source, avoid sharing mutable Random unsafely across threads, and log dice strategy configuration for audits.
Concurrency boundary
A single in-memory Game should process one turn at a time. If exposed over an API, protect playTurn with a per-game lock or command queue.
Persistence and recovery
Store initial board config plus ordered events. On restart, rebuild Board through the factory and replay accepted moves to restore positions.
What Interviewers Look For
Do you inject Dice instead of creating randomness inside Game?
Do you keep jump validation and overshoot rules inside Board?
Do you model turn order as a queue and rotate only non-winning players?
Do you separate board setup from game execution through a factory method?
Do you expose move and win notifications as observations rather than mixing UI with rules?
Can you explain why one Jump class is enough for both snakes and ladders?
Quiz
0/5 answered
1.Why should Game depend on the Dice interface instead of RandomDice directly?
2.What should happen when a player at 98 rolls 5 on a 100-cell board?
3.Why is a single Jump class enough for snakes and ladders?
4.Where should standard board layouts be constructed?
5.What is the best Observer seam in the reference Game class?
Practice Variants
Scripted dice and replay tests
BeginnerImplement a Dice that returns a fixed queue of rolls, then write tests for ladder, snake, overshoot, and exact win scenarios.
Configurable BoardFactory
IntermediateCreate a factory that reads dimension and jump pairs from a config object, calls Board.addJump, and fails fast on invalid layouts.
Observer-based scoreboard
IntermediateAdd GameEventObserver implementations for console logs and an in-memory scoreboard without letting observers change move outcomes.
Advanced turn rules
AdvancedAdd extra turns on rolling six and a maximum consecutive-six rule. Keep queue logic isolated behind a TurnRule strategy.
Flashcards
Cheat Sheet
Entities: Game, Board, Jump, Dice, RandomDice, Player, plus setup and event seams.
Patterns: Strategy for Dice, Factory Method for board setup, Observer for move and win events.
Turn flow: poll player → roll dice → compute target → apply one jump → set position → publish event → win or re-queue.
Invariants: final cell is dimension squared; jump starts are unique; jumps cannot start at first or final cell; overshoot keeps the player in place; winner is not re-queued.
Complexity: setup O(J + P) space; each turn O(1) time with a jump map; full game O(T) turns until win.
Extensions: scripted dice, config BoardFactory, event observers, chained jumps, extra-turn rules, persisted event replay.
References
- BookHead First Design Patterns — Freeman & Robson
- BookEffective Java — Joshua Bloch
- DocsRefactoring Guru — Strategy Pattern
- DocsRefactoring Guru — Observer Pattern