This is a classic LLD warm-up. It shows up early in loops at places like Amazon and Uber, before the hard systems round. The whole thing should take you 25 to 30 minutes end to end. The winning move is to model it cleanly, leave two obvious seams for change, and stop.
If you remember five things
The whole answer in one glance. Everything below defends these five calls.
- Unify snakes and ladders. Both are a jump from one cell to another, so collapse them into one
Jumpclass. Two classes that differ only by a sign are one class. - Rules live behind Strategy. "Exact landing," "bounce back," and "roll a 6 for an extra turn" are swappable
MoveStrategyimplementations. The turn loop never changes. - The board resolves the jump, in O(1). One map from cell to
Jump, onefinalPosition()call.Gamenever writesif snake else if ladder. - Inject the randomness. A seeded
Randomgoes intoDice, so tests are deterministic. Randomness you cannot control is randomness you cannot test. - Admit it is single-threaded. A turn-based game is sequential by nature. No locks inside
Game. Over-synchronizing a sequential loop is a worse signal than saying it is single-threaded.
1Clarify the requirements#
Pin the scope fast, because on an "easy" problem the trap is doing too much. I spend about 3 minutes here, no more.
Functional.
- A square board, usually 100 cells numbered 1 to 100.
- Two or more players take turns rolling a die and moving forward by the rolled value.
- Snakes move a player down (head to tail). Ladders move a player up (bottom to top).
- A player wins by landing on the last cell. I ask: do we overshoot or bounce back? I support both, default to "must land exactly."
- The game ends when someone wins.
Non-functional, the stuff I say out loud so the interviewer knows I am thinking.
- Board size, snakes, ladders, and number of dice are configurable. Hard-coding 100 is a red flag.
- The rules (what happens on a roll, what counts as a win) are swappable. People love adding twists like "roll a 6 to get an extra turn."
- Single process, in-memory. No persistence, no network, no cross-machine concurrency. I say that explicitly so nobody thinks I forgot. If this were a hosted multiplayer server I would scope it differently, and I name that boundary so the interviewer can push on it.
A few clarifiers I actually ask, because a "no" on half of these saves ten minutes of building the wrong thing.
- Can a snake's head sit on the last cell (cell 100)?
- Can two players share a cell?
- Is a 6 special?
I write the answers in the corner of the board so I can point back to them when I justify a choice later.
I am grading this opening before you draw a box. On an easy problem I want to see you scope down, not scope up. Name the single-process, in-memory boundary yourself, call out what is configurable, and ask the die and overshoot questions. Start adding a database or a lobby service nobody asked for and I have already decided you will over-build the real systems too.
2Identify the core entities#
Each class earns its place, and I can say in one sentence why each exists.
- Board. Holds the cells and knows where snakes and ladders are. It answers one real question: "if I land here, where do I actually end up?" That single responsibility is the whole reason it exists.
- Snake and Ladder. Both are just a jump from one cell to another. I unify them behind a small abstraction called
Jumpso the board does not care which is which. A snake jumps down, a ladder jumps up. Same shape, opposite sign. - Dice. Produces a roll. Configurable count, so two dice means a range of 2 to 12.
- Player. A name and a current position. That is it. Resisting the urge to put game logic in here is a signal in itself.
- Game. The orchestrator. Owns the board, dice, the player queue, and the turn loop.
- MoveStrategy and the rules. A pluggable piece that decides how a roll turns into a new position and whether that is a win. The "exact landing vs overshoot" decision lives here, so I can change the ruleset without touching the loop.
Do not make separate Snake and Ladder classes with duplicate logic. They are the same thing pointing in opposite directions. Split them and you hand yourself two copies of validation and resolution code to keep in sync. Collapsing them into Jump is the move the grader is waiting to see.
3Class design#
The center of gravity is Board.
- It stores a map from cell number to a
Jump. - When a roll puts a player on a cell, the board checks that map and returns the final destination in one call, in O(1).
Gamenever writesif snake ... else if ladder. It just asks the board.
Dice is tiny, but I keep it as a class so I can inject a fake in tests.
- Randomness you cannot control is randomness you cannot test.
- "Did you make the dice deterministic" is one of the five things I grade at the end.
The turn loop lives in Game.
- It pulls the next player, rolls, asks the
MoveStrategyfor the new position, updates the player, and checks for a win. - Keeping move logic in a strategy means I can change the win condition without touching the loop.
Game does not own: no snake class, no ladder class, no win rule. It composes a Board, a Dice, and a MoveStrategy, and every behavior that an interviewer might ask to change lives behind one of those three seams.And the runtime flow for a single turn, because an interviewer often wants to see the sequence:
finalPosition call inside the strategy, so Game never learns what a snake is. Swap the strategy and this exact sequence still holds.Reveal the answer
MoveStrategy, not the loop. If the win and overshoot rules sit in Game, then "bounce back" or "roll a 6 for an extra turn" forces you to edit the loop every time. Putting them behind the strategy interface means Game is written once and never touched again. That is the whole payoff of the pattern, and reaching for it before you are asked is the staff-level signal on an easy problem.4Key implementation#
I show the parts that carry the design. Getters, constructors, and the obvious stuff I would write live but will not paste in full.
The unified jump. A snake is just a jump whose end is below its start.
public class Jump {
private final int start;
private final int end;
public Jump(int start, int end) {
if (start == end) {
throw new IllegalArgumentException("A jump cannot start and end on the same cell");
}
this.start = start;
this.end = end;
}
public int start() { return start; }
public int end() { return end; }
public boolean isSnake() { return end < start; }
}
The board holds the jumps and resolves a landing in one place. This is the method the rest of the code leans on, and keeping it to one hop (not a loop) is a deliberate default I defend below.
public class Board {
private final int size;
private final Map<Integer, Jump> jumps = new HashMap<>();
public Board(int size) {
this.size = size;
}
public void addJump(Jump jump) {
if (jump.start() < 1 || jump.start() > size || jump.end() < 1 || jump.end() > size) {
throw new IllegalArgumentException("Jump is off the board");
}
if (jump.start() == size) {
throw new IllegalArgumentException("A jump cannot start on the winning cell");
}
if (jumps.containsKey(jump.start())) {
throw new IllegalArgumentException("Two jumps cannot share a start cell: " + jump.start());
}
jumps.put(jump.start(), jump);
}
// The one method everyone calls: land on a cell, get where you really end up.
public int finalPosition(int cell) {
Jump jump = jumps.get(cell);
return jump == null ? cell : jump.end();
}
public int size() { return size; }
}
Dice, small and injectable. The second constructor takes a Random so a test can seed it and get the same rolls every run.
public class Dice {
private final int count;
private final Random rng;
public Dice(int count) {
this(count, new Random());
}
public Dice(int count, Random rng) {
this.count = count;
this.rng = rng;
}
public int roll() {
int total = 0;
for (int i = 0; i < count; i++) {
total += rng.nextInt(6) + 1;
}
return total;
}
}
The move rules behind an interface. The default is "you must land exactly on the last cell."
public interface MoveStrategy {
int nextPosition(int current, int roll, Board board);
boolean hasWon(int position, Board board);
}
public class ExactLandingStrategy implements MoveStrategy {
@Override
public int nextPosition(int current, int roll, Board board) {
int candidate = current + roll;
if (candidate > board.size()) {
return current; // overshoot, you don't move
}
return board.finalPosition(candidate);
}
@Override
public boolean hasWon(int position, Board board) {
return position == board.size();
}
}
A second rule you can swap in if the interviewer asks, where overshooting bounces you back off the last cell.
public class BounceBackStrategy implements MoveStrategy {
@Override
public int nextPosition(int current, int roll, Board board) {
int candidate = current + roll;
if (candidate > board.size()) {
candidate = board.size() - (candidate - board.size());
}
return board.finalPosition(candidate);
}
@Override
public boolean hasWon(int position, Board board) {
return position == board.size();
}
}
The game, which is just a clean loop. Notice it contains no snake or ladder logic at all, and no win rule either. Both live elsewhere.
public class Game {
private final Board board;
private final Dice dice;
private final Queue<Player> players = new LinkedList<>();
private final MoveStrategy strategy;
private boolean finished = false;
public Game(Board board, Dice dice, MoveStrategy strategy) {
this.board = board;
this.dice = dice;
this.strategy = strategy;
}
public void addPlayer(Player player) {
if (finished) {
throw new IllegalStateException("Game already over");
}
players.add(player);
}
public Player play() {
if (players.size() < 2) {
throw new IllegalStateException("Need at least two players");
}
while (!finished) {
Player current = players.poll();
takeTurn(current);
players.add(current);
if (strategy.hasWon(current.position(), board)) {
finished = true;
return current;
}
}
return null;
}
private void takeTurn(Player player) {
int roll = dice.roll();
int next = strategy.nextPosition(player.position(), roll, board);
player.move(next);
}
}
Every piece has one job. The loop reads top to bottom like a sentence: poll a player, take a turn, requeue, check for a win.
The loop you never have to edit again is the one that earns the pass.
5Design decisions and patterns used#
The patterns are not decoration. Each one buys back a specific kind of change the interviewer is about to ask for.
- Strategy for the move rules. "Exact landing," "bounce back," and "roll a 6 for an extra turn" are all different strategies. The
Gameloop never changes. This is the single most valuable decision in the whole design, and it costs me one interface. - Composition over inheritance everywhere.
Gameis built from aBoard, aDice, and aMoveStrategy. I am not subclassing anything to get behavior. - A small unifying abstraction with Jump. Collapsing snakes and ladders into one concept removed a whole branch of duplicate code. When two classes differ only by a sign, they are one class.
- Builder or Factory for setup, which I mention but do not always code. A
GameBuilderthat takes board size, a list of jumps, and dice count keeps construction readable:new GameBuilder().boardSize(100).withSnake(99, 7).withLadder(2, 38).build(). If the interviewer wants preset boards ("classic," "quick game"), a factory method fits. - Dependency injection for randomness. Passing a seeded
RandomintoDicemakes the game deterministic in tests. One line of design, hours of debugging saved.
I skip the Observer pattern unless asked. For a console game, event listeners are ceremony. If they want a UI or a move log, I add an observer then and explain why it was not there before.
6Handling edge cases and concurrency#
The sneaky bits that separate a passing design from a sloppy one.
- A jump that lands on another jump. Real boards sometimes have a ladder dropping you onto a snake's head. My
finalPositionresolves exactly one hop. I ask whether chaining is allowed. If yes, I loop until the cell has no jump, and I guard against an infinite cycle by capping the hops or rejecting a looping config ataddJumptime. - Snake head or ladder bottom on the last cell. That makes the game unwinnable or trivial, so
addJumprejects any jump starting on cellsize. I would also reject a jump starting on cell 1 if the interviewer says the first cell is special. - Overshoot. Already handled by the strategy. The important thing is that I made it an explicit, named decision instead of a silent bug.
- Single winner. Once someone wins, the loop stops and
addPlayeris locked. No "second place" turns. - Bad config. Duplicate jump starts, off-board cells, fewer than two players. I fail loud in the constructor or
addJump, not three turns into the game.
Concurrency: for this problem there usually is none, and I say so.
- A turn-based board game is sequential by nature. One player moves at a time.
- If someone pushes for a server hosting many games, the clean answer is one game per thread or actor, with the
Gameobject owned by a single thread. - I would not add locks inside
Game. The real contention would be the matchmaking layer, not the dice.
Sprinkling synchronized across Game, spinning up a thread pool for the players, or reaching for a database to "persist state" on a children's board game. Over-engineering an inherently sequential loop tells me you cannot tell load-bearing from decoration, which is exactly the judgment the hard rounds are testing.
7Extensions and follow-ups#
Where interviewers push, and how I bend. The theme in every answer: the extension slots into an existing seam.
- "Roll a 6 gives an extra turn." New
MoveStrategy, or a small flag in the loop that re-queues the player to the front. Strategy keeps the loop clean. - "Different board sizes or 3D boards."
Boardalready takes a size. A non-square board just changes how I validate and render. The jump map does not care. - "Add a move history or scoreboard." Now Observer earns its keep.
GamepublishesonMoveandonWinevents, and a logger or UI subscribes. - "Persist and resume a game." Serialize player positions, whose turn it is, and the board config. The board is immutable after setup, so I only snapshot the mutable turn state.
- "Multiple dice with custom faces."
Dicegrows a configurable face count, or I pass in aSupplier<Integer>for truly custom dice.
That is the payoff of the strategy and the unified jump: every follow-up is an add, never a rewrite.
"A ladder drops you onto a snake's head. Do you chain the jumps?"
finalPosition resolves exactly one hop, which is the common rule. If the interviewer wants chaining, I loop until the cell has no jump, and I guard the infinite cycle by either capping the hop count or rejecting a looping configuration at addJump time. I make it a named decision, not a silent one."How do you test a game that depends on dice?"
Random through the second Dice constructor, so every run produces the same rolls. For a scripted game I can go further and pass a Dice fake that returns a fixed sequence, which lets me drive a player straight onto a snake or the winning cell and assert the outcome."How do you add a roll-a-6-gets-another-turn rule without touching the loop?"
MoveStrategy, or a small signal the strategy returns that tells Game to re-queue the current player to the front instead of the back. The loop body stays "poll, take a turn, requeue, check win." The rule change is isolated behind the interface."Make it a server hosting thousands of concurrent games. What changes?"
Game object itself stays single-threaded and owned by one thread or actor, because a single game is sequential. What I add around it is a matchmaking and session layer, plus persistence of the mutable turn state. I do not put locks inside Game. The contention lives in matchmaking, not the dice."Why one Jump class instead of a Snake and a Ladder?"
isSnake() derives from end < start. Two classes would mean two copies of the off-board validation and the resolution code, kept in sync by hand. One class removes a whole branch of duplication.What you can now explain in the room
- Why snakes and ladders collapse into one
Jumpclass, and what duplication that kills. - How the Strategy pattern keeps the turn loop free of win and overshoot logic.
- Why the board resolves a landing in O(1) with a single map lookup, and when you would loop for chained jumps.
- How injecting a seeded
Randommakes the whole game deterministic in tests. - Which bad configurations you reject up front in
addJump, and why you fail loud early. - Why the game is single-threaded by nature, and where concurrency actually belongs if you host many games.
If you liked the strategy seam here, try Design a Parking Lot and Design an Elevator System next, and read up on the Strategy and Factory patterns while you are at it.
Get a prep plan built around your Design Snake and Ladder interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.