Skip to content
Use code for 50% offSee plans

Low-Level Design (LLD) Questions: Parking Lot, Elevator, and More

Low-Level Design (LLD) Questions: Parking Lot, Elevator, and More

Complete LLD interview guide: parking lot, elevator, vending machine, and library system designs with Python code, SOLID principles, and design patterns.

By PhantomCodeAI Team

TL;DR

  • Low-Level Design interviews test object-oriented thinking, SOLID principles, and the ability to translate vague requirements into clean, extensible code.
  • The four canonical problems — parking lot, elevator, vending machine, and library system — repeatedly appear at Google, Amazon, Meta, and Apple senior rounds.
  • Master these patterns: enum-based state, separation of concerns across Vehicle/Spot/Level classes, strategy and factory patterns, and SOLID compliance, and you can handle any LLD prompt.

Introduction

Low-Level Design (LLD) questions test your ability to translate real-world requirements into clean, extensible code. Unlike system design (which focuses on databases, caching, and scalability), LLD focuses on object-oriented design, design patterns, and good architecture. LLD questions appear in senior engineering interviews at Google, Amazon, Meta, and Apple. If you can write clean LLD code, you stand out. This guide walks you through the most common LLD problems with complete solutions.

LLD vs. System Design

System Design (HLD): How would you design YouTube? Focus on databases, scalability, distribution. For that round, see our guide on how to prepare for system design interviews.

Low-Level Design (LLD): How would you design a parking lot system? Focus on classes, methods, design patterns, and clean code.

LLD questions are about writing code that's extensible, maintainable, and follows SOLID principles.

Key Design Patterns for LLD

Before diving into specific problems, understand these patterns:

Strategy Pattern: Different algorithms implementing the same interface.

Factory Pattern: Create objects without specifying exact classes.

Observer Pattern: Objects notify others about state changes.

Singleton Pattern: Exactly one instance of a class.

Adapter Pattern: Make incompatible interfaces work together.

Decorator Pattern: Add functionality to objects dynamically.

Problem 1: Design a Parking Lot

Requirements:

  • Parking lot has multiple levels, each with multiple spots
  • Spots have different sizes: compact, regular, large
  • Vehicles have different sizes: motorcycle, car, truck
  • Users can park, unpark, and check available spots
  • System tracks occupied and available spots

Design:

from enum import Enum
from datetime import datetime
from typing import Optional

class VehicleSize(Enum):
    MOTORCYCLE = 1
    CAR = 2
    TRUCK = 3

class ParkingSpotSize(Enum):
    COMPACT = 1
    REGULAR = 2
    LARGE = 3

class Vehicle:
    def __init__(self, license_plate: str, size: VehicleSize):
        self.license_plate = license_plate
        self.size = size

class ParkingSpot:
    def __init__(self, level: int, spot_number: int, size: ParkingSpotSize):
        self.level = level
        self.spot_number = spot_number
        self.size = size
        self.is_available = True
        self.vehicle: Optional[Vehicle] = None

    def park(self, vehicle: Vehicle) -> bool:
        if not self.is_available or not self._can_fit_vehicle(vehicle):
            return False
        self.is_available = False
        self.vehicle = vehicle
        return True

    def unpark(self) -> Vehicle:
        vehicle = self.vehicle
        self.is_available = True
        self.vehicle = None
        return vehicle

    def _can_fit_vehicle(self, vehicle: Vehicle) -> bool:
        if self.size == ParkingSpotSize.COMPACT:
            return vehicle.size == VehicleSize.MOTORCYCLE
        elif self.size == ParkingSpotSize.REGULAR:
            return vehicle.size in [VehicleSize.MOTORCYCLE, VehicleSize.CAR]
        else:  # LARGE
            return True

class ParkingLevel:
    def __init__(self, level: int, num_compact: int, num_regular: int, num_large: int):
        self.level = level
        self.spots = []

        spot_number = 0
        for _ in range(num_compact):
            self.spots.append(ParkingSpot(level, spot_number, ParkingSpotSize.COMPACT))
            spot_number += 1
        for _ in range(num_regular):
            self.spots.append(ParkingSpot(level, spot_number, ParkingSpotSize.REGULAR))
            spot_number += 1
        for _ in range(num_large):
            self.spots.append(ParkingSpot(level, spot_number, ParkingSpotSize.LARGE))
            spot_number += 1

    def park(self, vehicle: Vehicle) -> bool:
        for spot in self.spots:
            if spot.park(vehicle):
                return True
        return False

    def unpark(self, vehicle: Vehicle) -> bool:
        for spot in self.spots:
            if spot.vehicle == vehicle:
                spot.unpark()
                return True
        return False

    def available_spots_by_size(self, size: ParkingSpotSize) -> int:
        return sum(1 for spot in self.spots if spot.is_available and spot.size == size)

class ParkingLot:
    def __init__(self, num_levels: int, spots_per_level_config: list):
        self.levels = []
        for i in range(num_levels):
            config = spots_per_level_config[i]
            self.levels.append(ParkingLevel(i, config['compact'], config['regular'], config['large']))

    def park(self, vehicle: Vehicle) -> bool:
        for level in self.levels:
            if level.park(vehicle):
                return True
        return False

    def unpark(self, vehicle: Vehicle) -> bool:
        for level in self.levels:
            if level.unpark(vehicle):
                return True
        return False

    def get_available_spots(self) -> dict:
        spots = {'compact': 0, 'regular': 0, 'large': 0}
        for level in self.levels:
            spots['compact'] += level.available_spots_by_size(ParkingSpotSize.COMPACT)
            spots['regular'] += level.available_spots_by_size(ParkingSpotSize.REGULAR)
            spots['large'] += level.available_spots_by_size(ParkingSpotSize.LARGE)
        return spots

Key design decisions:

  • Vehicle and ParkingSpot are separate classes
  • ParkingLevel manages multiple spots
  • ParkingLot manages multiple levels
  • Encapsulation: each class knows only what it needs
  • Easy to extend: add new vehicle sizes or spot types

Problem 2: Design an Elevator System

Requirements:

  • Multiple elevators in a building
  • Elevators can move up/down
  • Users can request elevators from different floors
  • System should efficiently assign elevators

Design:

from enum import Enum
from collections import deque

class Direction(Enum):
    UP = 1
    DOWN = -1
    IDLE = 0

class Request:
    def __init__(self, floor: int):
        self.floor = floor

class Elevator:
    def __init__(self, elevator_id: int, max_floor: int):
        self.elevator_id = elevator_id
        self.current_floor = 0
        self.max_floor = max_floor
        self.direction = Direction.IDLE
        self.requests = deque()

    def add_request(self, request: Request):
        if 0 <= request.floor <= self.max_floor and request.floor != self.current_floor:
            self.requests.append(request)
            self._update_direction()

    def _update_direction(self):
        if not self.requests:
            self.direction = Direction.IDLE
            return

        next_floor = self.requests[0].floor
        if next_floor > self.current_floor:
            self.direction = Direction.UP
        elif next_floor < self.current_floor:
            self.direction = Direction.DOWN

    def move(self):
        if self.direction == Direction.IDLE:
            return

        self.current_floor += self.direction.value

        # Check if we've reached a requested floor
        if self.requests and self.requests[0].floor == self.current_floor:
            self.requests.popleft()
            self._update_direction()

    def is_moving(self) -> bool:
        return self.direction != Direction.IDLE

class ElevatorSystem:
    def __init__(self, num_elevators: int, max_floor: int):
        self.elevators = [Elevator(i, max_floor) for i in range(num_elevators)]
        self.max_floor = max_floor

    def request_elevator(self, floor: int):
        elevator = self._find_best_elevator(floor)
        if elevator:
            elevator.add_request(Request(floor))

    def _find_best_elevator(self, floor: int) -> Optional[Elevator]:
        # Closest idle elevator, or closest moving elevator
        idle_elevators = [e for e in self.elevators if not e.is_moving()]

        if idle_elevators:
            return min(idle_elevators, key=lambda e: abs(e.current_floor - floor))

        # All elevators moving; pick closest
        return min(self.elevators, key=lambda e: abs(e.current_floor - floor))

    def step(self):
        # Simulate time step
        for elevator in self.elevators:
            elevator.move()

Key design decisions:

  • Direction enum represents elevator state
  • Elevator maintains queue of requests
  • ElevatorSystem chooses best elevator for each request
  • Extensible: easy to add new assignment strategies

Problem 3: Design a Vending Machine

Requirements:

  • Users select items and insert money
  • System dispenses items if paid enough
  • System returns change
  • Track inventory

Design:

from enum import Enum

class Item:
    def __init__(self, name: str, price: float, quantity: int):
        self.name = name
        self.price = price
        self.quantity = quantity

class Coin(Enum):
    PENNY = 0.01
    NICKEL = 0.05
    DIME = 0.10
    QUARTER = 0.25

class VendingMachine:
    def __init__(self):
        self.inventory = {}
        self.money_inserted = 0
        self.coins = {}

    def add_item(self, location: str, item: Item, quantity: int):
        if location not in self.inventory:
            self.inventory[location] = item
        self.inventory[location].quantity += quantity

    def insert_coin(self, coin: Coin):
        self.money_inserted += coin.value
        self.coins[coin] = self.coins.get(coin, 0) + 1

    def select_item(self, location: str) -> str:
        if location not in self.inventory:
            return "Invalid location"

        item = self.inventory[location]

        if item.quantity == 0:
            return "Out of stock"

        if self.money_inserted < item.price:
            return f"Insufficient funds. Need ${item.price:.2f}, have ${self.money_inserted:.2f}"

        # Dispense item
        item.quantity -= 1
        change = self.money_inserted - item.price
        self.money_inserted = 0

        return f"Dispensed {item.name}. Change: ${change:.2f}"

    def return_money(self) -> float:
        change = self.money_inserted
        self.money_inserted = 0
        self.coins = {}
        return change

Problem 4: Design a Library Management System

Requirements:

  • Books can be borrowed and returned
  • Users have a borrowing limit
  • Track due dates and fines
  • Search for books by title or author

Design:

from datetime import datetime, timedelta
from typing import List

class User:
    def __init__(self, user_id: str, name: str):
        self.user_id = user_id
        self.name = name
        self.borrowed_books = []
        self.max_borrow_limit = 5

    def can_borrow(self) -> bool:
        return len(self.borrowed_books) < self.max_borrow_limit

    def borrow(self, book: 'Book') -> bool:
        if not self.can_borrow():
            return False
        self.borrowed_books.append(book)
        return True

    def return_book(self, book: 'Book') -> bool:
        if book in self.borrowed_books:
            self.borrowed_books.remove(book)
            return True
        return False

class Book:
    def __init__(self, isbn: str, title: str, author: str):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.is_available = True
        self.borrowed_by: Optional[User] = None
        self.due_date: Optional[datetime] = None

    def borrow(self, user: User) -> bool:
        if not self.is_available:
            return False
        self.is_available = False
        self.borrowed_by = user
        self.due_date = datetime.now() + timedelta(days=14)
        return user.borrow(self)

    def return_book(self) -> float:
        fine = self._calculate_fine()
        self.is_available = True
        self.borrowed_by = None
        self.due_date = None
        return fine

    def _calculate_fine(self) -> float:
        if datetime.now() <= self.due_date:
            return 0.0
        days_late = (datetime.now() - self.due_date).days
        return days_late * 0.5  # 50 cents per day

class Library:
    def __init__(self):
        self.books = {}
        self.users = {}

    def add_book(self, book: Book):
        if book.isbn not in self.books:
            self.books[book.isbn] = []
        self.books[book.isbn].append(book)

    def search_by_title(self, title: str) -> List[Book]:
        return [b for books in self.books.values() for b in books if title.lower() in b.title.lower()]

    def search_by_author(self, author: str) -> List[Book]:
        return [b for books in self.books.values() for b in books if author.lower() in b.author.lower()]

    def borrow_book(self, user_id: str, isbn: str) -> bool:
        user = self.users.get(user_id)
        if not user or isbn not in self.books:
            return False

        for book in self.books[isbn]:
            if book.is_available and book.borrow(user):
                return True

        return False

    def return_book(self, user_id: str, isbn: str) -> float:
        user = self.users.get(user_id)
        if not user:
            return -1

        for book in user.borrowed_books:
            if book.isbn == isbn:
                user.return_book(book)
                fine = book.return_book()
                return fine

        return -1

Common LLD Mistakes

Mistake 1: Poor encapsulation Exposing internal details; let classes manage their own state.

Mistake 2: God objects One class doing too much. Distribute responsibilities.

Mistake 3: Ignoring SOLID principles S: Single responsibility, O: Open/closed, L: Liskov substitution, I: Interface segregation, D: Dependency inversion.

Mistake 4: No extensibility Hard-code behavior instead of using patterns that allow easy extension.

Mistake 5: Mixing concerns Don't mix business logic with I/O or persistence.

LLD Interview Tips

  1. Clarify requirements. Ask about edge cases and constraints.
  2. Design before coding. Sketch classes and relationships on paper/whiteboard.
  3. Use design patterns. They show you understand good design.
  4. Write clean code. Use proper naming, methods, and encapsulation.
  5. Think extensibility. "What if we add a new requirement?"
  6. Handle edge cases. Empty lists, null values, invalid inputs.
  7. Test your design. Walk through scenarios mentally.

Practicing LLD Effectively

  1. Solve 15-20 LLD problems, warming up with LeetCode design problems such as LRU Cache and Design Parking System
  2. For each, design before coding
  3. Code cleanly with proper OOP
  4. Consider scalability and extensibility
  5. Refactor after solving

Preparing for LLD Interviews

LLD interviews test whether you can write production-quality code. When you're practicing LLD design and want feedback on your object-oriented architecture, PhantomCodeAI can help by providing design suggestions and catching architectural issues in real-time through its chat interface. While it's primarily designed for algorithmic interviews, the underlying technology can help with code quality feedback. Plans start at ₹1,599 at phantomcodeai.com.

Final Thoughts

Low-Level Design is about writing code that's clean, extensible, and follows best practices. Master the problems in this guide and you'll be ready for LLD interviews at top tech companies.

Frequently asked questions

What's the difference between LLD and HLD (system design)?

HLD focuses on databases, caching, sharding, and distributed-system tradeoffs at scale. LLD focuses on classes, methods, design patterns, and SOLID principles within a single service or application.

Which design patterns show up most in LLD interviews?

Strategy (swappable algorithms), Factory (object creation), Observer (state-change notifications), Singleton (single instance), and Decorator (dynamic behavior). Knowing when to apply each is more important than memorizing the GoF catalog.

How long should you spend designing before coding in an LLD round?

Spend 5-10 minutes clarifying requirements and sketching classes and relationships. Don't write a single line of code until your interviewer agrees the class diagram is reasonable — it's much harder to refactor 30 lines of bad Python under time pressure.

What's the most common LLD interview mistake?

God objects — one class doing everything. Distribute responsibility across small, focused classes (Vehicle, ParkingSpot, ParkingLevel, ParkingLot) so each one is easy to reason about and extend.

How many LLD problems should you practice before a senior interview?

Solve 15-20 LLD problems end to end. After about a dozen, you'll start seeing the same building blocks (enums for state, registry patterns, allocation strategies) and pattern recognition kicks in.