Introduction to Low-Level Design (LLD) & Interview Mastery
Low-Level Design (LLD) translates system requirements and architectural blueprints into modular, extensible, and clean code. While High -Level Design (HLD) defines distributed services, data flow boundaries, network topologies, and scaling infrastructure, LLD focuses squarely on classes, object-oriented interfaces, design patterns, algorithmic data structures, and thread safety within a targeted component. Success in a 45-minute LLD interview requires disciplined time management, dynamic scope clarification, and avoiding common anti-patterns like over-engineering class diagrams or writing boilerplate Data Access Layers (DALs).
1. High-Level Design (HLD) vs. Low-Level Design (LLD)
System design operates in two primary phases across the software development lifecycle: HLD outlines the architectural vision, while LLD executes the actual implementation mechanics.
Architectural Comparison
Note: In an LLD interview, treat persistence layers pragmatically. Never write boilerplate Data Access Objects (DAOs) or complex database connectivity scripts. Assume in-memory stores (such as an array or map) unless the interviewer explicitly directs you to model an internal storage engine.
2. Understanding System Boundaries: The Movie Ticket Booking System
Consider a standard system design problem: The Movie Ticket Booking System (frequently asked at companies like Microsoft, Uber, and early-to-mid-stage startups).
The HLD Request Lifecycle
In an HLD interview, the discussion centers on macroscopic traffic routing and data consistency:
The LLD In-Process Decomposition
In an LLD interview, you discard external infrastructure discussions. You dive straight into the internal mechanics of the service handling the domain logic (e.g., the BookingService running inside the application server):
LLD asks:
- How are
Show,Screen, andSeatmodeled to avoid redundancy? - How is a seat prevented from being double-booked when two users tap "Checkout" at the exact same millisecond?
- How do we extend the system from movie reservations to sporting events or live theater concerts without rewriting core booking engines?
3. The 45-Minute LLD Interview Execution Framework
The primary pitfall in LLD interviews is poor time management. Candidates often spend 25 minutes drawing structural diagrams or writing getter/setter methods, leaving no time to write the algorithmic business logic that interviewers evaluate.
Phase 1: Clarify Requirements via Plain-English Flows (Minutes 0–5)
- Do not draw UML or activity diagrams.
- Write down the chronological flow of actions from the end-user's perspective in simple markdown notes.
- Actively confirm bounds with the interviewer:
"Should I assume payments are handled through an external third-party gateway, or should I model internal processing strategies?" "Should we focus primarily on seat allocation concurrency, or do you want catalog search filtering?"
Phase 2: Derive Core Domain Entities (Minutes 5–12)
- Extract nouns directly from your Phase 1 plain-English walk-through.
- List entities as lightweight classes. Exclude defensive getters, setters, and serialization tags (
private final, Lombok annotations). - Announce your decision:
"To maximize our coding time on core business logic, I am omitting boilerplate getters, setters, and constructors, keeping fields package-private for now."
Phase 3: Core Method Signatures & Interfaces (Minutes 12–20)
- Extract verbs from your Phase 1 walk-through.
- Define service contracts:
bookSeats(...),lockSeats(...),processPayment(...). - Establish relationship bindings (e.g., whether
Seatis coupled toScreenor dynamically bound to an activeShow).
Phase 4: Implement Core Logic & Algorithmic Engines (Minutes 20–35)
- Write clean, working code for the focal problem.
- Implement thread safety using robust language features (e.g., Java locks, synchronizations, concurrent structures).
- Honor fundamental design principles: keep classes Single-Responsibility, avoid duplicated logic (DRY), and write straightforward routines (KISS).
Phase 5: Mention Patterns & Decouple (Minutes 35–42)
- Explicitly state how design patterns apply to the components you built (e.g., Strategy for payments, Factory for booking generation).
- If time is limited, verbalize and declare structural hooks rather than writing full concrete subclasses.
Phase 6: Edge Cases & Wrap-Up (Minutes 42–45)
- Walk through your code with a dry-run tracing multiple threads or edge cases.
- Explain how errors, booking timeouts, and race conditions are mitigated.
4. Requirement Gathering via User Journey Mapping
Rather than using formal Activity Diagram modeling tools, write an end-to-end user journey in plain English during the interview.
Plain English Flow: Movie Ticket Booking
Entity Extraction from Domain Nouns
By reading directly through the journey map, extract the fundamental domain nouns:
User: The account holder placing the order.Movie: The content being screened (title, runtime, language).Theater: The physical venue holding one or more screens.Screen: The individual auditorium with a geometric layout of physical seats.Show: A specific temporal instance of aMoviescreened on aScreen.Seat: The physical seat representation (row, column, category like VIP or Economy).SeatLock: The transient state locking a seat for a specific checkout session.Booking: The transactional domain record capturing the reservation state.Payment: The financial exchange tracking completion, failure, or refund.Ticket: The finalized receipt generated after successful booking.
5. Domain Modeling & Entity Definition
When writing entity classes under interview constraints, eliminate boilerplate syntax. Focus exclusively on identifiers, key fields, and structural associations.
Entity Relationships
Modeling Insight: A common trap is placing booking status directly on the physical
Seatclass attached to aScreen. A single physical screen hosts multiple shows per day (). IfisBookedis a field ofSeat, booking seatA1for an afternoon show invalidates it for an evening show.Therefore, separate the static metadata (
PhysicalSeat) from the temporal, contextual instance (ShowSeator an active state mapped within aShow).
Clean Interview Entity Code
import java.util.List;
// Minimal representation of domain entities for interview clarity
class Movie {
String id;
String title;
int durationMinutes;
}
enum SeatCategory {
ECONOMY, PREMIUM, VIP
}
class PhysicalSeat {
String id;
int row;
int number;
SeatCategory category;
}
class Screen {
String id;
String name;
List<PhysicalSeat> seats;
}
class Theater {
String id;
String name;
String city;
List<Screen> screens;
}
class Show {
String id;
Movie movie;
Screen screen;
long startTimeEpochMs;
}
enum BookingStatus {
PENDING_PAYMENT, CONFIRMED, CANCELLED, EXPIRED
}
class Booking {
String id;
String userId;
Show show;
List<PhysicalSeat> bookedSeats;
BookingStatus status;
double totalAmount;
}
6. Business Logic, Concurrency, and Core Implementation
The core problem in a ticket booking system is preventing double bookings under high concurrency. If two users attempt to lock seat C4 at the exact same millisecond, exactly one must succeed, and the other must receive an explicit rejection.
The Seat Locking State Machine
Naive Implementation (Race Condition Prone)
import java.util.HashMap;
import java.util.Map;
// NAIVE IMPLEMENTATION: Vulnerable to race conditions
class NaiveBookingManager {
private Map<String, String> seatReservations = new HashMap<>(); // seatId -> userId
public boolean reserveSeat(String seatId, String userId) {
// Race window: Thread 1 and Thread 2 can both evaluate this condition simultaneously
if (!seatReservations.containsKey(seatId)) {
seatReservations.put(seatId, userId);
return true;
}
return false;
}
}
If Thread 1 and Thread 2 hit !seatReservations.containsKey(seatId) at the same time, both read false, both execute put(...), and the last thread silently overwrites the first. This creates a double booking bug in production.
Decoupled Production-Grade Implementation
We resolve race conditions using an isolated in-memory locking service backed by synchronization primitives (ReentrantLock or ConcurrentHashMap with atomic methods):
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
class SeatLock {
String seatId;
String lockedByUserId;
long lockTimeEpochMs;
long timeoutDurationMs;
public SeatLock(String seatId, String lockedByUserId, long lockTimeEpochMs, long timeoutDurationMs) {
this.seatId = seatId;
this.lockedByUserId = lockedByUserId;
this.lockTimeEpochMs = lockTimeEpochMs;
this.timeoutDurationMs = timeoutDurationMs;
}
public boolean isExpired() {
return System.currentTimeMillis() > (this.lockTimeEpochMs + this.timeoutDurationMs);
}
}
class InMemorySeatLockService {
// Lock granularity: showId -> Map of (seatId -> SeatLock)
private final Map<String, Map<String, SeatLock>> showLocks = new ConcurrentHashMap<>();
private final ReentrantLock globalLock = new ReentrantLock();
private static final long DEFAULT_LOCK_TIMEOUT_MS = 300_000; // 5 minutes
public boolean tryLockSeats(String showId, List<String> seatIds, String userId) {
globalLock.lock();
try {
showLocks.putIfAbsent(showId, new HashMap<>());
Map<String, SeatLock> currentLocks = showLocks.get(showId);
// Step 1: Verify all requested seats are unreserved or have expired locks
for (String seatId : seatIds) {
if (currentLocks.containsKey(seatId)) {
SeatLock activeLock = currentLocks.get(seatId);
if (!activeLock.isExpired()) {
return false; // Contention detected: at least one seat is actively held
}
}
}
// Step 2: Atomically assign locks across all requested seats
long now = System.currentTimeMillis();
for (String seatId : seatIds) {
currentLocks.put(seatId, new SeatLock(seatId, userId, now, DEFAULT_LOCK_TIMEOUT_MS));
}
return true;
} finally {
globalLock.unlock();
}
}
public void releaseLocks(String showId, List<String> seatIds, String userId) {
globalLock.lock();
try {
Map<String, SeatLock> currentLocks = showLocks.get(showId);
if (currentLocks == null) return;
for (String seatId : seatIds) {
SeatLock activeLock = currentLocks.get(seatId);
if (activeLock != null && activeLock.lockedByUserId.equals(userId)) {
currentLocks.remove(seatId);
}
}
} finally {
globalLock.unlock();
}
}
}
class SeatUnavailableException extends RuntimeException {
public SeatUnavailableException(String message) {
super(message);
}
}
class BookingCoordinator {
private final InMemorySeatLockService lockService;
private final Map<String, Booking> bookingStore = new ConcurrentHashMap<>();
public BookingCoordinator(InMemorySeatLockService lockService) {
this.lockService = lockService;
}
public Booking createBooking(String userId, Show show, List<PhysicalSeat> seats) {
List<String> seatIds = new ArrayList<>();
for (PhysicalSeat s : seats) {
seatIds.add(s.id);
}
// Attempt locking phase
boolean locked = lockService.tryLockSeats(show.id, seatIds, userId);
if (!locked) {
throw new SeatUnavailableException("One or more selected seats are currently locked by another transaction.");
}
// Assemble unconfirmed domain booking
Booking booking = new Booking();
booking.id = UUID.randomUUID().toString();
booking.userId = userId;
booking.show = show;
booking.bookedSeats = seats;
booking.status = BookingStatus.PENDING_PAYMENT;
booking.totalAmount = calculateTotal(seats);
bookingStore.put(booking.id, booking);
return booking;
}
private double calculateTotal(List<PhysicalSeat> seats) {
// Simple mapping: VIP = $20, else $10
double total = 0.0;
for (PhysicalSeat seat : seats) {
total += (seat.category == SeatCategory.VIP) ? 20.0 : 10.0;
}
return total;
}
}
7. Design Principles vs. Design Patterns
Candidates often conflate design principles with design patterns. Differentiating between them keeps your code clean and helps structure your verbal explanations to the interviewer.
Essential Principles
- DRY (Don't Repeat Yourself): Consolidate redundant calculations—such as seat pricing, coordinate math, or status checks—into centralized helper functions.
- KISS (Keep It Simple, Stupid): Do not implement premature microservice abstractions or custom messaging protocols when a simple list or standard collection is sufficient.
- YAGNI (You Aren't Gonna Need It): Skip auxiliary capabilities like social sharing, custom user profile pictures, or loyalty reward points unless the interviewer explicitly adds them to the scope.
- SOLID:
- Single Responsibility Principle (SRP): Isolate concerns so that
SeatLockServicemanages transient state locks, whilePaymentProcessorhandles checkout executions. - Open/Closed Principle (OCP): Design core systems so new behavior (like a new payment method) can be added via new classes without modifying existing business algorithms.
- Single Responsibility Principle (SRP): Isolate concerns so that
8. High-Yield Design Patterns for LLD Interviews
You do not need to memorize all 23 Gang of Four patterns for an LLD round. Focusing on four core patterns will handle the vast majority of interview scenarios.
Concrete Strategy Pattern: Decoupling Payment Processing
// Strategy interface
interface PaymentStrategy {
boolean processPayment(String transactionId, double amount);
}
class CreditCardPaymentStrategy implements PaymentStrategy {
public boolean processPayment(String transactionId, double amount) {
// Connect to payment gateway API
return true;
}
}
class UPIPaymentStrategy implements PaymentStrategy {
public boolean processPayment(String transactionId, double amount) {
// Connect to Virtual Payment Address / UPI handler
return true;
}
}
// Payment Context
class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public boolean executePayment(String transactionId, double amount) {
if (strategy == null) {
throw new IllegalStateException("Payment strategy not selected.");
}
return strategy.processPayment(transactionId, amount);
}
}
Interview Tip: If time is running low, write the
interface PaymentStrategyand show how it plugs into your orchestrator. Then tell the interviewer: "I am utilizing the Strategy Pattern here to satisfy the Open/Closed Principle. If we add PayPal or crypto later, we can implement new strategies without modifying our core checkout methods." This demonstrates architectural mastery without consuming precious coding minutes.
9. The Intersection of LLD and Data Structures & Algorithms (DSA)
Modern tech evaluations increasingly combine DSA with Low-Level Design. Companies avoid purely academic algorithmic problems in favor of domain-driven systems that require concrete data structure implementations under clean object-oriented abstractions.
Analytical Complexity: Seat Lock Management
Consider managing active seat locks with expiration tracking:
- Linear Scan: Iterating an array of locks to find expired items takes time per check.
- Priority Queue Structure: Storing records in a Min-Heap keyed by
lockTimeEpochMs + timeoutDurationMsgives access to the earliest-expiring seat and for insertions and deletions.
10. Navigating Counter-Questions, Trapdoors, and Edge Cases
During implementation, interviewers often introduce edge cases to assess how your design responds to changing requirements.
Critical Edge Cases to Address
- Deadlock in Batch Seat Reservations:
- Scenario: User attempts to lock
[Seat-1, Seat-2]. Simultaneously, User attempts to lock[Seat-2, Seat-1]. - Failure: If locks are acquired sequentially, a cyclic wait can form, causing a deadlock:
- Scenario: User attempts to lock
- Resolution: Enforce a global resource-ordering invariant. Always sort the list of target seat IDs lexicographically before acquiring locks:
JAVA
// Enforce deterministic lock ordering to prevent cyclic waits Collections.sort(requestedSeatIds);
-
The Partial Failure Problem:
- Scenario: A user reserves three seats, but the third fails validation.
- Resolution: Design seat locking as an all-or-nothing transaction. If any seat is unavailable, rollback and release all previously acquired locks in the batch before returning an error.
-
Orphaned Locks (Abandoned Sessions):
- Scenario: A user locks seats but closes their browser tab without paying or canceling.
- Resolution: Add explicit expiration timestamps to locks. Expired locks are treated as available during conflict checks, and a background cleaner thread periodically prunes expired entries.
Summary and Key Takeaways
- LLD Solves In-Process Code Extensibility: While HLD addresses horizontal system scaling across servers, LLD ensures class structures, interfaces, and algorithms remain maintainable, extensible, and clean.
- Lead with Plain-English Journeys: Skip complex UML and activity diagrams. Map user actions chronologically in simple markdown to clarify scope and extract entities naturally.
- Entities are Simple Data Containers: In a 45-minute interview, avoid boilerplate constructors, getters, and setters. Focus your time on writing the core domain logic.
- Separate Static Layouts from Dynamic Contexts: Decouple physical structural layouts (
Screen,PhysicalSeat) from contextual, time-bound states (Show,SeatLock). - Anticipate Concurrency Early: Identify multi-threaded contention points (such as concurrent seat reservations) right away, and protect critical sections using thread-safe structures or explicit lock management.
- Principles Trump Patterns: Apply SOLID, DRY, and KISS continuously while writing code. Use Design Patterns (Strategy, Factory, Observer) purposefully to solve concrete decoupling challenges.
- Communicate Proactively: Maintain an active dialogue with your interviewer throughout the round. Confirm scope and edge cases before writing code to ensure your implementation matches what they are evaluating.