Low Level Design: Snake and Food Game System Architecture
A comprehensive low-level design (LLD) and object-oriented analysis for a scalable Snake and Food Game. This architecture bridges foundational Object-Oriented Design (OOD) patterns with optimized Data Structures & Algorithms (DSA), reflecting real-world technical interview expectations at top-tier companies like Uber.
1. Context & Interview Problem Framing (LLD vs. DSA)
In high-bar software engineering interviews (such as Uber, Google, and Amazon), board game designs are common evaluative tools. Unlike pure LLD problems (e.g., Splitwise or Elevator System) where domain modeling and state transitions take priority, the Snake and Food Game sits uniquely at the intersection of Low-Level Object-Oriented Design and Algorithmic Optimizations (similar to LeetCode #353: Design Snake Game).
The Dual-Focus Challenge
- Low-Level Design (LLD): Demonstrating clean code separation, design patterns (Strategy, Factory, Singleton, Observer), extensibility, and maintainability.
- Data Structures & Algorithms (DSA): Selecting optimal data structures to execute snake movement, body updates, and collision detection in time complexity per tick.
Note: Because the core mechanics are algorithmically driven, candidates must deliver a working, optimal DSA core while wrapping it inside a decoupled, production-grade object-oriented architecture.
2. Core Mental Model & Classic Analogy
Real-Life Analogy: The Nokia 3310 Snake Game
The core mental hook originates from the classic Nokia mobile snake game:
- A grid matrix serves as the world boundary.
- A snake composed of connected segments moves across cells in discrete time steps.
- The player controls only the direction of the head; the entire body follows the head's trajectory.
- Food items spawn across the board. Consuming food extends the snake's tail length and increases score.
- Collision with outer walls or with the snake's own trailing body causes immediate death.
3. System Requirements & Clarifying Questions
To establish boundaries during an interview, always partition requirements into Functional and Non-Functional specifications after asking clarifying questions.
Functional Requirements
- Grid Board: The game takes place on an grid.
- Snake Control: Supports 4 directional moves:
UP,DOWN,LEFT, andRIGHT. Controlled by human players or automated AI strategies. - Food Mechanics: Food appears on the board. Consuming food increases score and length. Different food items (Normal, Bonus, Poisonous) introduce varied score increments and penalties.
- Collision Detection: Game terminates upon hitting a boundary (in fixed-wall mode) or colliding with the snake's own body segments.
- Score & Event Tracking: Real-time tracking of current score with listener notifications for UI or external systems.
Non-Functional Requirements
- Performance: Board movement, food consumption, and collision checking must run in time per move.
- Extensibility: Design must support boundary variations (e.g., wrap-around edge traversal without death) and flexible food spawn rules without modifying core game loop code.
- Thread Safety & Decoupling: Separation of input handling, state processing, and render/event notifications.
4. Key Entities & Algorithmic Data Structure Selection
To make movement , data structures must support fast insertion at the head, deletion at the tail, and membership checks for collision.
Data Structure Selection Rationale
5. Architectural Design Patterns Applied
6. High-Level System Flow
The execution workflow illustrates how player input translates into positional state updates, collision physics checks, and event notifications.
7. Implementation: Core Domain Models & Strategy Pattern
Position Coordinate Model
import java.util.Objects;
public class Position {
private final int row;
private final int col;
public Position(int row, int col) {
this.row = row;
this.col = col;
}
public int getRow() { return row; }
public int getCol() { return col; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Position position = (Position) o;
return row == position.row && col == position.col;
}
@Override
public int hashCode() {
return Objects.hash(row, col);
}
@Override
public String toString() {
return "(" + row + ", " + col + ")";
}
}
Strategy Pattern for Movement
public enum Direction {
UP, DOWN, LEFT, RIGHT
}
public interface MovementStrategy {
Position getNextPosition(Position currentHead, Direction direction);
}
// Human strategy applying direct mapping
public class HumanMovementStrategy implements MovementStrategy {
@Override
public Position getNextPosition(Position currentHead, Direction direction) {
int r = currentHead.getRow();
int c = currentHead.getCol();
switch (direction) {
case UP: return new Position(r - 1, c);
case DOWN: return new Position(r + 1, c);
case LEFT: return new Position(r, c - 1);
case RIGHT: return new Position(r, c + 1);
default: throw new IllegalArgumentException("Unknown direction: " + direction);
}
}
}
// AI strategy capable of computing autonomous decisions
public class AIMovementStrategy implements MovementStrategy {
@Override
public Position getNextPosition(Position currentHead, Direction direction) {
// Conceptually: Scans available valid neighbor positions avoiding immediate walls
// Falls back to human mapping logic for basic simulation
int r = currentHead.getRow();
int c = currentHead.getCol();
return new Position(r + 1, c);
}
}
8. Implementation: Food Creation via Factory Pattern
public abstract class FoodItem {
private final Position position;
private final int pointValue;
public FoodItem(Position position, int pointValue) {
this.position = position;
this.pointValue = pointValue;
}
public Position getPosition() { return position; }
public int getPointValue() { return pointValue; }
}
public class NormalFood extends FoodItem {
public NormalFood(Position position) {
super(position, 1);
}
}
public class SpecialFood extends FoodItem {
public SpecialFood(Position position) {
super(position, 3);
}
}
public class PoisonousFood extends FoodItem {
public PoisonousFood(Position position) {
super(position, -1);
}
}
// Factory encapsulating object creation
public class FoodFactory {
public enum FoodType { NORMAL, SPECIAL, POISONOUS }
public static FoodItem createFood(FoodType type, Position position) {
switch (type) {
case SPECIAL: return new SpecialFood(position);
case POISONOUS: return new PoisonousFood(position);
case NORMAL:
default: return new NormalFood(position);
}
}
}
9. Implementation: Singleton Game Board & Deque Snake Logic
Game Board (Singleton)
public class GameBoard {
private static GameBoard instance;
private final int height;
private final int width;
private GameBoard(int height, int width) {
this.height = height;
this.width = width;
}
public static synchronized GameBoard getInstance(int height, int width) {
if (instance == null) {
instance = new GameBoard(height, width);
}
return instance;
}
public static GameBoard getInstance() {
if (instance == null) {
throw new IllegalStateException("GameBoard is not initialized. Call getInstance(h, w) first.");
}
return instance;
}
public boolean isOutOfBounds(Position pos) {
int r = pos.getRow();
int c = pos.getCol();
return r < 0 || r >= height || c < 0 || c >= width;
}
public int getHeight() { return height; }
public int getWidth() { return width; }
}
Snake Entity Representation
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
public class Snake {
private final Deque<Position> body;
private final Set<Position> occupiedPositions;
public Snake(Position initialHeadPosition) {
this.body = new LinkedList<>();
this.occupiedPositions = new HashSet<>();
// Initialize Snake with head at default position
this.body.addFirst(initialHeadPosition);
this.occupiedPositions.add(initialHeadPosition);
}
public Position getHead() {
return body.peekFirst();
}
public Position getTail() {
return body.peekLast();
}
public boolean occupies(Position pos) {
return occupiedPositions.contains(pos);
}
public void addHead(Position newHead) {
body.addFirst(newHead);
occupiedPositions.add(newHead);
}
public Position removeTail() {
Position removedTail = body.removeLast();
occupiedPositions.remove(removedTail);
return removedTail;
}
public Deque<Position> getBody() {
return body;
}
}
10. Implementation: The Main SnakeGame Logic & Game Loop
This module ties together the algorithms, state transitions, collision detection, and food processing.
import java.util.List;
import java.util.ArrayList;
public class SnakeGame {
private final GameBoard board;
private final Snake snake;
private final List<FoodItem> foodQueue;
private int foodIndex;
private int score;
private boolean isGameOver;
private final MovementStrategy movementStrategy;
private final List<GameObserver> observers;
public SnakeGame(int height, int width, List<FoodItem> foodQueue, MovementStrategy movementStrategy) {
this.board = GameBoard.getInstance(height, width);
this.snake = new Snake(new Position(0, 0));
this.foodQueue = foodQueue;
this.foodIndex = 0;
this.score = 0;
this.isGameOver = false;
this.movementStrategy = movementStrategy;
this.observers = new ArrayList<>();
}
public void registerObserver(GameObserver observer) {
observers.add(observer);
}
public int move(Direction direction) {
if (isGameOver) {
System.out.println("Game Over! Cannot execute further moves.");
return -1;
}
Position currentHead = snake.getHead();
Position newHead = movementStrategy.getNextPosition(currentHead, direction);
// 1. Boundary Wall Collision Check
if (board.isOutOfBounds(newHead)) {
handleGameOver("Collision with Wall at " + newHead);
return -1;
}
// 2. Identify tail location before collision evaluation
Position currentTail = snake.getTail();
// 3. Self-Collision Check
// Note: New head CAN move into the tail's current cell IF the snake is not eating food on this move,
// because the tail will vacate that cell simultaneously.
boolean leavesTail = false;
if (foodIndex < foodQueue.size()) {
FoodItem targetFood = foodQueue.get(foodIndex);
if (!targetFood.getPosition().equals(newHead)) {
leavesTail = true;
}
} else {
leavesTail = true;
}
if (snake.occupies(newHead)) {
// Collision occurs if newHead matches any body part EXCLUDING the tail about to be evicted
if (!(leavesTail && newHead.equals(currentTail))) {
handleGameOver("Self Collision at " + newHead);
return -1;
}
}
// 4. Food Consumption Analysis
boolean ateFood = false;
if (foodIndex < foodQueue.size() && foodQueue.get(foodIndex).getPosition().equals(newHead)) {
ateFood = true;
FoodItem eatenFood = foodQueue.get(foodIndex);
score += eatenFood.getPointValue();
foodIndex++;
notifyFoodEaten(eatenFood);
}
// 5. Update Snake Body Logic
snake.addHead(newHead);
if (!ateFood) {
snake.removeTail(); // Normal step: maintain constant length
}
notifyMove(newHead, score);
return score;
}
private void handleGameOver(String reason) {
this.isGameOver = true;
System.out.println("GAME OVER: " + reason);
for (GameObserver observer : observers) {
observer.onGameOver(score, reason);
}
}
private void notifyMove(Position newHead, int currentScore) {
for (GameObserver observer : observers) {
observer.onMove(newHead, currentScore);
}
}
private void notifyFoodEaten(FoodItem food) {
for (GameObserver observer : observers) {
observer.onFoodConsumed(food);
}
}
public boolean isGameOver() { return isGameOver; }
public int getScore() { return score; }
}
11. Edge Cases, Collision Physics & Complexity Analysis
Critical Edge Cases
- The Tail-Chasing Edge Case:
- Scenario: The snake's head moves directly into the space currently occupied by its tail segment.
- Resolution: If no food is consumed during the step, the tail segment will move forward and vacate its cell in the same turn. Therefore, moving into the current tail cell is a valid move, provided
newHead.equals(currentTail)during a non-food-eating turn.
- Rapid Direction Inversion:
- Scenario: Moving
RIGHTand immediately executingLEFTin the next tick causes instant self-collision with the neck segment. - Resolution: Validate and suppress inverse directional moves prior to positional state recalculation.
- Scenario: Moving
- Empty Food Queue / Exhaustion:
- Scenario: Snake consumes all items pre-populated in
foodQueue. - Resolution: Game gracefully shifts to survival mode or dynamically generates randomized food items on unallocated grid cells.
- Scenario: Snake consumes all items pre-populated in
Algorithmic Complexity Evaluation
- Time Complexity:
move()operation:- Direction calculation:
- Boundary test:
HashSetself-collision check:Dequeinsertion/deletion:
- Space Complexity:
- , where is the current length of the snake (stored in
Deque+HashSet) and is the number of queued food items.
- , where is the current length of the snake (stored in
12. Architectural Extensibility: Boundary Wrap-Around & Observer Event Tracking
Extensibility Feature A: Infinite Boundary Wrap-Around
In several commercial variations (and specialized interview counter-questions), hitting a wall does not cause death. Instead, the snake wraps around to the opposite edge of the board.
public class WrapAroundMovementStrategy implements MovementStrategy {
private final int boardHeight;
private final int boardWidth;
public WrapAroundMovementStrategy(int boardHeight, int boardWidth) {
this.boardHeight = boardHeight;
this.boardWidth = boardWidth;
}
@Override
public Position getNextPosition(Position currentHead, Direction direction) {
int r = currentHead.getRow();
int c = currentHead.getCol();
switch (direction) {
case UP: r = r - 1; break;
case DOWN: r = r + 1; break;
case LEFT: c = c - 1; break;
case RIGHT: c = c + 1; break;
}
// Apply modulo wrapping arithmetic
int wrappedRow = (r + boardHeight) % boardHeight;
int wrappedCol = (c + boardWidth) % boardWidth;
return new Position(wrappedRow, wrappedCol);
}
}
Extensibility Feature B: Event Tracking via Observer Pattern
public interface GameObserver {
void onMove(Position newHeadPosition, int currentScore);
void onFoodConsumed(FoodItem foodItem);
void onGameOver(int finalScore, String reason);
}
public class ConsoleGameLogger implements GameObserver {
@Override
public void onMove(Position newHeadPosition, int currentScore) {
System.out.println("[EVENT] Snake moved to " + newHeadPosition + " | Current Score: " + currentScore);
}
@Override
public void onFoodConsumed(FoodItem foodItem) {
System.out.println("[EVENT] Food Consumed (+ " + foodItem.getPointValue() + " pts)");
}
@Override
public void onGameOver(int finalScore, String reason) {
System.out.println("[LOG] Game ended. Final Score: " + finalScore + " | Reason: " + reason);
}
}
13. LLD vs. High-Level Design Synthesis
To conclude an interview effectively, contrast how this Low-Level Design seamlessly integrates into a full High-Level System Architecture.
Summary and Key Takeaways
- Dual Nature: Snake Game is primarily a DSA-centric LLD problem where algorithmic choices directly determine system performance.
- Movement Execution: Combining a
Deque(for quick head/tail operations) with aHashSet(for constant-time self-collision checks) ensures optimal move latency. - Strategy Pattern for Controls: Decouples direction inputs from positional calculations, making it easy to swap between human controls, wrap-around logic, and autonomous AI agents.
- Factory Pattern for Items: Encapsulates spawning rules for different food types (Normal, Bonus, Poisonous) without modifying core move logic.
- Tail Eviction Handling: When moving without consuming food, the tail cell must be freed before performing self-collision checks to allow tail-chasing moves.
- Observer Event Decoupling: Isolates real-time game updates from UI renderers, console loggers, and analytics pipelines.
- Extensible Architecture: Modulo arithmetic easily replaces static wall collisions with boundary wrap-around movement without altering the main game loop.