Skip to content
1 min read

Design the Snake Game

Interview Context

Asked at: Google, Amazon, Apple | Level: L4-L6 | Time: 45 minutes | Type: LLD/OOP Design


Requirements

Functional

  • Snake moves continuously in current direction on a grid board
  • Player changes direction via keyboard input (up/down/left/right)
  • Snake grows by one unit when it eats food
  • Game ends when snake hits wall or its own body
  • Food spawns randomly on unoccupied cells
  • Score increases with each food eaten

Non-Functional

  • Smooth movement at configurable tick rate
  • O(1) collision detection for self-hit
  • No food spawns on snake body
  • Game state easily serializable (for replay/save)

Class Diagram

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
    class Game {
        -Board board
        -Snake snake
        -Food food
        -GameState state
        -int score
        -int tickRate
        +start() void
        +tick() GameState
        +changeDirection(Direction dir) void
    }

    class Snake {
        -Deque~Position~ body
        -Direction currentDirection
        +move() Position
        +grow() void
        +getHead() Position
        +containsPosition(Position pos) boolean
    }

    class Board {
        -int width
        -int height
        +isWithinBounds(Position pos) boolean
        +getRandomEmptyCell(Snake snake) Position
    }

    class Food {
        -Position position
        -int points
        +respawn(Board board, Snake snake) void
    }

    class Position {
        -int x
        -int y
        +move(Direction dir) Position
    }

    class Direction {
        <<enumeration>>
        UP
        DOWN
        LEFT
        RIGHT
        +isOpposite(Direction other) boolean
    }

    class GameState {
        <<enumeration>>
        RUNNING
        PAUSED
        GAME_OVER
    }

    Game --> Board
    Game --> Snake
    Game --> Food
    Game --> GameState
    Snake --> Position
    Snake --> Direction
    Food --> Position
    Board --> Position

Key Design Decisions

Decision Choice Why
Snake body Deque (LinkedList) O(1) add head, O(1) remove tail for movement
Self-collision check HashSet of positions O(1) lookup instead of O(n) list scan
Direction change Ignore opposite Prevents instant self-collision (can't reverse)
Food placement Random with rejection Simple; board is sparse enough for fast retry
Game loop Tick-based Decouples rendering from logic; testable

Java Implementation

Java
public enum Direction {
    UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0);

    private final int dx, dy;
    Direction(int dx, int dy) { this.dx = dx; this.dy = dy; }

    public boolean isOpposite(Direction other) {
        return this.dx + other.dx == 0 && this.dy + other.dy == 0;
    }
    public int getDx() { return dx; }
    public int getDy() { return dy; }
}

public enum GameState { RUNNING, PAUSED, GAME_OVER }

public record Position(int x, int y) {
    public Position move(Direction dir) {
        return new Position(x + dir.getDx(), y + dir.getDy());
    }
}

public class Snake {
    private final Deque<Position> body = new ArrayDeque<>();
    private final Set<Position> bodySet = new HashSet<>(); // O(1) collision
    private Direction currentDirection;
    private boolean shouldGrow = false;

    public Snake(Position start) {
        body.addFirst(start);
        bodySet.add(start);
        currentDirection = Direction.RIGHT;
    }

    public void setDirection(Direction dir) {
        // Prevent 180-degree reversal
        if (!dir.isOpposite(currentDirection)) {
            this.currentDirection = dir;
        }
    }

    public Position move() {
        Position newHead = body.peekFirst().move(currentDirection);
        body.addFirst(newHead);
        bodySet.add(newHead);

        if (shouldGrow) {
            shouldGrow = false; // tail stays — snake grows
        } else {
            Position tail = body.removeLast();
            bodySet.remove(tail);
        }
        return newHead;
    }

    public void grow() { shouldGrow = true; }
    public Position getHead() { return body.peekFirst(); }
    public boolean containsPosition(Position pos) { return bodySet.contains(pos); }
    public int length() { return body.size(); }
}
Java
public class Game {
    private final Board board;
    private final Snake snake;
    private Food food;
    private GameState state;
    private int score;

    public Game(int width, int height) {
        this.board = new Board(width, height);
        Position center = new Position(width / 2, height / 2);
        this.snake = new Snake(center);
        this.food = new Food(board.getRandomEmptyCell(snake));
        this.state = GameState.RUNNING;
        this.score = 0;
    }

    public void changeDirection(Direction dir) {
        if (state == GameState.RUNNING) {
            snake.setDirection(dir);
        }
    }

    // Called every game tick (e.g., 100ms intervals)
    public GameState tick() {
        if (state != GameState.RUNNING) return state;

        Position newHead = snake.move();

        // Check wall collision
        if (!board.isWithinBounds(newHead)) {
            state = GameState.GAME_OVER;
            return state;
        }

        // Check self collision (head is already in bodySet after move)
        // If body contains newHead at more than one position, it's a hit
        if (Collections.frequency(List.copyOf(snake.getBody()), newHead) > 1) {
            state = GameState.GAME_OVER;
            return state;
        }

        // Check food collision
        if (newHead.equals(food.getPosition())) {
            snake.grow();
            score += food.getPoints();
            food = new Food(board.getRandomEmptyCell(snake));
        }

        return state;
    }

    public int getScore() { return score; }
}

public class Board {
    private final int width, height;
    private final Random random = new Random();

    public Board(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public boolean isWithinBounds(Position pos) {
        return pos.x() >= 0 && pos.x() < width && pos.y() >= 0 && pos.y() < height;
    }

    public Position getRandomEmptyCell(Snake snake) {
        Position pos;
        do {
            pos = new Position(random.nextInt(width), random.nextInt(height));
        } while (snake.containsPosition(pos));
        return pos;
    }
}

public class Food {
    private final Position position;
    private final int points;

    public Food(Position position) {
        this.position = position;
        this.points = 10;
    }

    public Position getPosition() { return position; }
    public int getPoints() { return points; }
}
Java
public class GameLoop implements Runnable {
    private final Game game;
    private final int tickRateMs;
    private final Consumer<GameSnapshot> renderer;

    public GameLoop(Game game, int tickRateMs, Consumer<GameSnapshot> renderer) {
        this.game = game;
        this.tickRateMs = tickRateMs;
        this.renderer = renderer;
    }

    @Override
    public void run() {
        while (game.tick() != GameState.GAME_OVER) {
            renderer.accept(game.snapshot());
            try { Thread.sleep(tickRateMs); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
        }
        renderer.accept(game.snapshot()); // final frame
    }
}

// Snapshot for rendering — immutable view of game state
public record GameSnapshot(
    List<Position> snakeBody,
    Position food,
    int score,
    GameState state
) {}

SOLID Principles Applied

Principle How Applied
S — Single Responsibility Snake manages body movement; Board handles bounds; Game orchestrates
O — Open/Closed New food types (bonus, poison) extend Food without changing Game
L — Liskov Substitution Any renderer (Consumer<GameSnapshot>) works — console, GUI, network
I — Interface Segregation Consumer<GameSnapshot> is minimal; game logic has no UI dependency
D — Dependency Inversion GameLoop depends on Consumer interface for rendering, not concrete UI

Interview Walkthrough (45 minutes)

Time What to Do
0-5 min Clarify: board size, wrapping walls vs. death, food types, multiplayer?
5-15 min Draw class diagram — Snake (Deque), Board, Food, Game, Direction
15-25 min Explain: Deque for O(1) move, HashSet for O(1) collision, direction reversal guard
25-35 min Code: Snake.move(), Game.tick(), Board.getRandomEmptyCell()
35-45 min Discuss: special food types, increasing speed, replay system, multiplayer extension