How to answer the classic LLD rounds in Java — parking lot, elevator system, BookMyShow, URL shortener, file system, Splitwise, vending machine, chess, ATM, logging framework, notification system, cache with eviction, ride-sharing app, and a distributed ID generator — with entities, patterns, key classes, and the concurrency and extensibility points interviewers probe.
Published September 25, 2026
A 45–90 minute LLD round is scored on:
Say what you're leaving out. Below, each problem gives the design skeleton and the points interviewers probe; the linked pages have full walkthroughs.
Short answer:
ParkingLot → Levels → ParkingSpots (with a type: compact, regular, large, EV, disabled); Vehicle (an abstract class, or a type enum); Ticket; EntryGate and ExitGate; Payment.ConcurrentLinkedDeque), or claim spots atomically (CAS on the spot status, or a lock per level). Persisted versions use a conditional database update.interface SpotAllocationStrategy { Optional<ParkingSpot> allocate(VehicleType type); }
interface PricingStrategy { Money price(Ticket t, Instant exit); }
class ParkingSpot {
private final String id; private final SpotType type;
private final AtomicReference<Vehicle> occupant = new AtomicReference<>();
boolean tryOccupy(Vehicle v) { return occupant.compareAndSet(null, v); } // atomic claim
void release() { occupant.set(null); }
}
Learn it in depth → Parking Lot
Short answer:
ElevatorController (the dispatcher), Elevator (current floor, direction, state: idle, moving up, moving down, doors open, maintenance), Request (hall calls, with a direction, and car calls), Door, Display.TreeSets), and serves in its current direction before reversing (LOOK), which avoids starvation.Learn it in depth → Elevator System
Short answer:
City → Cinema → Screen → Seat (row, number, type); Movie; Show (a movie on a screen at a time); ShowSeat (the seat state per show: available, locked, booked, with its price); Booking; Payment; User.ShowSeat status plus a lock expiry, updated atomically (UPDATE show_seat SET status='LOCKED', locked_by=?, lock_until=? WHERE show_id=? AND seat_id IN (...) AND status='AVAILABLE', then check that the row count equals the seats requested; or a Redis SET NX PX per seat);Learn it in depth → Movie Ticket Booking System
Short answer:
ShortenerService (shorten(longUrl, alias?, expiry?), resolve(code)), a CodeGenerator strategy, a UrlRepository, a cache, and analytics events.final class Base62 {
private static final char[] ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
static String encode(long id) {
StringBuilder sb = new StringBuilder();
do { sb.append(ALPHABET[(int) (id % 62)]); id /= 62; } while (id > 0);
return sb.reverse().toString();
}
}
Learn it in depth → Design a URL Shortener
Short answer:
Entry (name, parent, created and modified times, permissions), with File (content, size) and Directory (children: Map<String, Entry>, and its size is the sum of its children's).mkdir -p, ls (sorted), addContentToFile, readContentFromFile, rm, mv, and find (with a Visitor, or recursive search filtered by a Specification or predicate: name pattern, size, extension)./, and walk from the root (handling . and ..).ReadWriteLock for the tree; ConcurrentHashMap children for simple cases.abstract sealed class Entry permits File, Directory {
final String name; Directory parent;
Entry(String name) { this.name = name; }
abstract long size();
}
final class File extends Entry {
private final StringBuilder content = new StringBuilder();
File(String n) { super(n); }
long size() { return content.length(); }
void append(String s) { content.append(s); }
}
final class Directory extends Entry {
final Map<String, Entry> children = new TreeMap<>(); // sorted for ls
Directory(String n) { super(n); }
long size() { return children.values().stream().mapToLong(Entry::size).sum(); }
}
Learn it in depth → Composite & Proxy
Short answer:
User, Group, Expense (the payer or payers, the amount, participants, and a split type), Split (per user: an owed amount), BalanceSheet.EqualSplit, ExactSplit, PercentSplit, ShareSplit, each validating the input (the percentages sum to 100; the exact amounts sum to the total), and handling rounding (give the remainder paisa or cent to someone deterministically; use BigDecimal or long minor units).(debtor, creditor) → amount, or a net balance per user.Learn it in depth → Splitwise Expense Sharing
Short answer:
Idle, HasMoney, Dispensing, OutOfStock and Maintenance, and each handles insertCoin, selectProduct, dispense and refund differently, so there are no giant if/else blocks.Inventory (slot → product and count), Product (price), a Coin or Note enum, and a CashBox, with a change-making algorithm (greedy for canonical coin systems; DP in general).interface VendingState {
void insertMoney(VendingMachine m, int amount);
void select(VendingMachine m, String slot);
void cancel(VendingMachine m);
}
final class IdleState implements VendingState {
public void insertMoney(VendingMachine m, int amount) { m.addBalance(amount); m.setState(new HasMoneyState()); }
public void select(VendingMachine m, String slot) { throw new IllegalStateException("Insert money first"); }
public void cancel(VendingMachine m) { }
}
Learn it in depth → Vending Machine
Short answer:
Game (players, board, turn, status, move history), Board (an 8×8 grid of Cells), an abstract Piece (colour) with subclasses King, Queen, Rook, Bishop, Knight and Pawn, Move (from, to, piece, captured piece, promotion), Player (human or AI strategy).piece.legalMoves(board, from). Sliding pieces share direction-based logic. Then filter out moves that leave your own king in check (simulate them on a copy, or apply and undo).Learn it in depth → Chess Engine Design
Short answer:
ATM (cash dispenser, card reader, keypad, screen, printer), Card, Account, BankService (an interface to the bank: authentication, balance, debit), Transaction (withdrawal, deposit, balance inquiry, transfer).Idle → CardInserted → Authenticated → TransactionSelected → Dispensing → Idle, with a PIN-retry limit (then retain the card).Learn it in depth → ATM System
Short answer:
Logger (named, and hierarchical: com.app.booking inherits from com.app);Level (TRACE < DEBUG < INFO < WARN < ERROR), with a level check first (cheap when disabled; lazy Supplier<String> messages);LogEvent (timestamp, level, logger, thread, message, exception, MDC context);Appenders (console, file with rolling, remote), each with a Layout or Formatter (pattern, JSON);Filters.LoggerFactory.getLogger(name));Learn it in depth → Design a Logging Framework
Short answer:
Notification (recipient, template ID and parameters, channel preferences, priority), Channel (an interface: EmailChannel, SmsChannel, PushChannel), TemplateEngine, UserPreferenceService, NotificationService.send);Learn it in depth → Notification Observer / PubSub Design
Short answer:
Cache<K, V> with get, put, remove, an optional TTL, and statistics (hits, misses, evictions).EvictionPolicy<K> with keyAccessed(k), keyAdded(k), keyRemoved(k) and evict(). Implementations:
computeIfAbsent-style, with single-flight protection against a stampede), a maximum size or weight, and eviction listeners.Learn it in depth → Cache with Pluggable Eviction Policy
Short answer:
Rider, Driver (a vehicle, location, and status: offline, available, on trip), Location, RideRequest, Trip (a state machine: REQUESTED → DRIVER_ASSIGNED → ARRIVED → IN_PROGRESS → COMPLETED / CANCELLED), Fare, Payment, Rating.AVAILABLE → RESERVED) with a timeout if the driver doesn't accept, then offer the ride to the next driver.Learn it in depth → Ride Booking Class Model
Short answer:
synchronized nextId() increments the sequence within the same millisecond, and waits for the next millisecond when the sequence overflows.public final class SnowflakeIdGenerator {
private static final long EPOCH = 1_704_067_200_000L; // 2024-01-01T00:00:00Z
private final long machineId; private long lastMs = -1, sequence = 0;
public SnowflakeIdGenerator(long machineId) {
if (machineId < 0 || machineId > 1023) throw new IllegalArgumentException("machineId 0..1023");
this.machineId = machineId;
}
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastMs) throw new IllegalStateException("clock moved backwards by " + (lastMs - now) + " ms");
if (now == lastMs) {
sequence = (sequence + 1) & 0xFFF; // 12 bits
if (sequence == 0) while ((now = System.currentTimeMillis()) <= lastMs) Thread.onSpinWait();
} else sequence = 0;
lastMs = now;
return ((now - EPOCH) << 22) | (machineId << 12) | sequence;
}
}
Learn it in depth → Design a Distributed ID Generator
Q: How do you avoid over-engineering in an LLD round? A: Start with the minimal classes for the core use cases, apply a pattern only where it removes a real conditional or enables a stated extension, and verbally note further extensions instead of coding them all.
Q: Where should concurrency control live in these designs? A: At the point of contention: the spot, seat or driver being claimed. Use an atomic state transition (CAS, a conditional database update) rather than a global lock, so unrelated operations proceed in parallel.
Q: Interface or abstract class for entities like Piece or Vehicle?
A: Use an abstract class (or a sealed hierarchy) when there's shared state and behaviour (position, colour); interfaces for capabilities and strategies (PricingStrategy, Channel). Sealed classes (Java 17) let pattern-matching switch be exhaustive.
Q: How do you make an LLD testable?
A: Depend on interfaces (clock, ID generator, repositories, payment gateway), inject them through constructors, and keep the domain logic pure, so tests use fakes (a fixed Clock, an in-memory repository).