A 45-minute timeboxed playbook for any LLD prompt — from "design a parking lot" to "design a chess game" — with a full minute-by-minute walkthrough at the end.
Picture Raj at his SDE-3 onsite. The interviewer says, "design a movie booking system." Raj knows Strategy. He knows Factory. He's read about every GoF pattern twice. So he opens the whiteboard, draws a class called Movie, then a class called Theater, then a class called Booking — and freezes. The classes are right but the relationships are vague. He hasn't named the requirements. He hasn't decided whether Seat is a class or a field on Theater. He hasn't thought about race conditions on bookings. Twenty-five minutes in he's drawing arrows between boxes that he's not sure point in the right direction. The interviewer asks "what does book() actually do?" and Raj realises he hasn't decided who owns the seat-locking logic.
The problem isn't that Raj doesn't know patterns. The problem is he has no playbook for the order of operations. LLD interviews need structure even more than HLD ones, because LLD has more degrees of freedom — every entity could be a class or a field, every method could live on the producer or the consumer, every variation point could be a Strategy or a Factory or just a switch statement. Without an order, you'll thrash.
This page lays out the 5 steps, gives you the timebox for each, then shows them in action on a "design a parking lot" prompt. By the end you should be able to walk into any LLD interview and know exactly what you're going to do at minute 5, minute 15, minute 25, and minute 35.
Here's the playbook on one page. Five distinct phases, ordered the way a senior engineer would actually approach a new domain in a real design review. Each step has a hard timebox — the goal isn't to finish fast, it's to spend exactly the right amount on each phase so you reach the code with the interviewer still engaged.
Requirements. 3–5 functional + 3 non-functional. Explicit out-of-scope. Get sign-off before drawing.
Core Entities. 5–8 nouns with a one-sentence responsibility each. Build the vocabulary.
Class Diagram. UML — fields, methods, relationships. Inheritance vs composition justified.
Patterns + Sequence. Find variation points, apply 2–4 patterns, draw the happy-path sequence.
Java Code. Enums → interfaces → entities → service → main(). One end-to-end happy path that compiles.
Trade-offs & Q&A. Extension points, concurrency, "what would you change?"
The single most undervalued step. Candidates who skip it always design the wrong thing. The interviewer's gentle "interesting, but what about X?" twenty minutes in lands like a wrecking ball, and there's no time to recover. Five minutes here saves twenty later.
Phrase each as "a user should be able to..." Resist the urge to brainstorm 15 features. The interviewer doesn't have time to design 15 things in 45 minutes, and the loop is graded on depth, not breadth. Pick the 3 hardest, hardest-coupled features — they'll force out the most interesting design choices.
Three actions, each one drives a different architectural slice. Park → spot allocation strategy. Pay → billing strategy. Admin → extension points / Open-Closed.
You'll spend 15 minutes just listing features and have no time for the class diagram, let alone code. The interviewer will start cutting features by force, which is a process failure on you.
NFRs in LLD are smaller in scope than HLD ones — you don't need to estimate QPS — but you still need to commit to a few. The good list: concurrency model extensibility direction performance budget durability. Each one constrains a later design choice.
Saying "I'm not designing X" is as valuable as saying "I'm designing Y". It tells the interviewer you've considered the trade-off and made a deliberate cut. "Payment gateway integration is out of scope — I'll use a PaymentProvider interface so production swaps in Stripe; the LLD focuses on the in-memory model" earns more credit than silently leaving payments unaddressed.
List the data nouns the system manipulates. One sentence of responsibility per entity. No fields yet, no relationships, no methods — just names and what each one is responsible for. This step exists to establish vocabulary with the interviewer so when you say "Spot" or "Ticket" later, you both mean the same thing.
Read the requirements out loud. Every concrete noun that has its own state or its own lifecycle is an entity candidate. Adjectives and verbs are not entities — they become fields and methods. "Park a car in a spot" gives you Car, Spot (entities) and park() (method). "in" isn't an entity.
| Entity | Responsibility (one sentence) | Lifecycle |
|---|---|---|
| ParkingLot | Owns all floors, exposes the entry/exit operations | Singleton — one per facility |
| Floor | Holds a list of spots and answers "is anything free for this vehicle size?" | Created at lot setup, mostly immutable |
| Spot | Represents a single parking slot with size and occupancy state | Long-lived; oscillates FREE ↔ OCCUPIED |
| Vehicle | Represents the thing being parked; carries size + license plate | Transient — exists for one parking session |
| Ticket | The receipt that proves a vehicle entered; carries entry time | Issued on park, consumed on exit |
| Bill | Computed at exit from ticket + pricing strategy | One-shot — generated and paid |
Six entities is the sweet spot for a parking lot. Less than four and you're hiding complexity in fat classes; more than nine and you're overdesigning. If you're tempted to add EntryGate, ExitGate, and DisplayBoard — pause. Those are interface concerns, not domain entities. Mention them in passing if asked, but don't pollute the diagram.
When you draw park() in Step 5, you'll reach for "the Spot collection" — the interviewer already knows what that means.
If you can't list the entities, you don't understand the problem. Five minutes here exposes confusion early, while you can still recover.
"Slot" vs "Spot" vs "Bay" — pick one and the rest of the interview becomes shorter and clearer.
Now the entities get fields, methods, and relationships. This is where most of the LLD score is decided. The interviewer is watching for two things: (a) does the class structure cleanly map to the domain you described in Step 2, and (b) do the relationships use the right kind of arrow — inheritance vs composition vs aggregation vs association?
UML: solid line + hollow triangle (--|>). "A Car is a Vehicle." Use only when the subclass genuinely substitutes for the parent. Default away from inheritance — it's a one-way door.
UML: solid line + filled diamond (*--). "A Floor owns its Spots." If the parent dies, the children die with it. Spots can't exist without their floor.
UML: solid line + hollow diamond (o--). "A Garage has Cars." Cars exist before, during, and after they're parked. The garage doesn't own them.
UML: solid line with arrowhead (-->). "A Ticket references the Spot it's for." A weak link, no ownership. The most common relationship.
The diagram alone earns nothing if the interviewer can't follow why each arrow is the kind of arrow it is. As you draw, say: "ParkingLot composes Floor — floors don't exist outside their lot. Floor composes Spot for the same reason. Spot is abstract because Bike, Car, and Truck spots have different size constraints — that's inheritance because they truly substitute. PricingStrategy is an interface that ParkingLot holds by reference (association) because pricing rules change every quarter and I want a new rule to be a new file."
Now you find the variation points in the design — the places where requirements are most likely to change next quarter — and you apply the GoF pattern that absorbs that change cleanly. Then you draw the happy-path sequence so the interviewer can see the runtime, not just the static topology.
Ask one question per noun in the diagram: "if I had to add a new variant of this next month, would the existing code change?" If yes, that's a variation point. The pattern toolbox is in Part 3; the cheat-sheet for picking the right one is below.
| Smell | Pattern | Parking Lot Example |
|---|---|---|
An if/else over a "kind" enum | Strategy | Pricing rules — hourly, day/night, weekend |
| Object construction has many switches | Factory | Creating the right Spot subclass for a vehicle |
| One write must update many readers | Observer | Spot freed → notify reservation holders |
| Behaviour depends on lifecycle stage | State | Reservation: PENDING → CONFIRMED → EXPIRED |
| Optional features stack at runtime | Decorator | Peak-hour surcharge wraps base pricing |
| Exactly one instance, globally accessible | Singleton | The ParkingLot registry |
| Constructor with >3 params | Builder | Configuring a Lot at startup |
Pick the most important user action — usually the one from your top functional requirement — and draw the sequence of method calls between objects. A sequence diagram is worth ten paragraphs of explanation; the interviewer can see the runtime.
Notice two things in the sequence. One: the alt block makes the race condition visible. Two drivers hitting the same spot is a real risk and showing the CAS retry path scores points without anyone asking. Two: every participant is a real class from your Step 3 diagram. If a sequence message has no class to send it to, you're missing an entity.
Twelve minutes is not a lot. The temptation is to start with the most exciting class and write it in detail. The right move is the opposite — write the boring scaffolding first (enums, interfaces) so the meat in the middle has something to lean on, and end with one main() that exercises the happy path. If the interviewer can read your main() top-to-bottom and see the design come alive, you win the loop.
VehicleSize, SpotStatus, anything finite. Cheap to write, locks in vocabulary.
PricingStrategy, SpotAllocator, PaymentProvider. The variation points from Step 4.
Spot, Vehicle, Ticket. Records or POJOs — skip getter/setter ceremony.
ParkingLot — the orchestrator. Where the parking + billing logic lives.
Three lines: park a car, sleep, exit. Show the design works end-to-end.
enum VehicleSize { BIKE, CAR, TRUCK } interface PricingStrategy { double calculate(Duration stay, VehicleSize size); } interface SpotAllocator { Optional<Spot> findAndOccupy(VehicleSize size); }Entity classes — Spot is the abstract base, vehicle is a record
abstract class Spot { protected final String id; protected final VehicleSize size; protected final AtomicReference<Vehicle> occupant = new AtomicReference<>(); Spot(String id, VehicleSize size) { this.id = id; this.size = size; } boolean tryOccupy(Vehicle v) { return v.size().ordinal() <= size.ordinal() && occupant.compareAndSet(null, v); // race-safe } void vacate() { occupant.set(null); } } class CarSpot extends Spot { CarSpot(String id) { super(id, VehicleSize.CAR); } } class TruckSpot extends Spot { TruckSpot(String id) { super(id, VehicleSize.TRUCK); } } record Vehicle(String licensePlate, VehicleSize size) {} record Ticket(String id, Spot spot, Instant entry) {} record Bill(Ticket ticket, double amount, Duration stay) {}Service + main() — the happy path you can read top-to-bottom
class ParkingLot { private final SpotAllocator allocator; private final PricingStrategy pricing; private final Map<String, Ticket> openTickets = new ConcurrentHashMap<>(); ParkingLot(SpotAllocator allocator, PricingStrategy pricing) { this.allocator = allocator; this.pricing = pricing; } Ticket park(Vehicle v) { Spot spot = allocator.findAndOccupy(v.size()) .orElseThrow(() -> new LotFullException(v.size())); Ticket t = new Ticket(UUID.randomUUID().toString(), spot, Instant.now()); openTickets.put(t.id(), t); return t; } Bill exit(String ticketId) { Ticket t = openTickets.remove(ticketId); if (t == null) throw new UnknownTicketException(); Duration stay = Duration.between(t.entry(), Instant.now()); t.spot().vacate(); return new Bill(t, pricing.calculate(stay, t.spot().size), stay); } } public class Demo { public static void main(String[] a) throws Exception { SpotAllocator alloc = new SimpleAllocator(List.of(new CarSpot("S-1"))); PricingStrategy pricing = new HourlyPricing(40.0); ParkingLot lot = new ParkingLot(alloc, pricing); Ticket t = lot.park(new Vehicle("KA-01-AB-1234", VehicleSize.CAR)); Thread.sleep(3000); Bill b = lot.exit(t.id()); System.out.println("Pay: ₹" + b.amount()); } }
AtomicReference.compareAndSet on the spot makes the race-condition discussion concrete. (2) Open/Closed: a new PricingStrategy implementation is one new class — no edits to ParkingLot. (3) Composition over inheritance: ParkingLot holds SpotAllocator by reference instead of extending it. (4) Java 17+ idioms: record for value types, Optional for "not found", ConcurrentHashMap for the ticket registry. Each one is a senior signal even if the interviewer never asks.equals()/hashCode(), exception class definitions, the trivial SimpleAllocator body. Say out loud "I'd Lombok these in production" or "I'll skip the SimpleAllocator body — just iterates the spot list — happy to write it if you want." The interviewer almost always says skip it. Use the time saved on the methods that matter.Of all the things that separate a junior-level LLD answer from a senior one, the biggest is specificity. Junior candidates name a class; senior candidates name the class, the data structure inside it, the concurrency primitive, the failure mode, and the extension point. Same five seconds, ten times the signal.
Which list? ArrayList? Synchronised? Indexed how? Iterated under contention?
"Per-floor I'll keep an EnumMap<VehicleSize, ConcurrentLinkedDeque<Spot>> of free spots so allocation is O(1) and lock-free. Occupied spots leave the deque entirely; on vacate, they re-enter at the head. Re-entry at head means the same spot gets re-used most often, which keeps the working set hot and helps with cache-friendly lookups in tests."
Synchronised on what monitor? What's the contention pattern? What's the timeout?
"Each Spot holds an AtomicReference<Vehicle>. tryOccupy does a single CAS — if it returns false, the caller (the allocator) tries the next free spot. No blocking, no monitor, no deadlock surface. The whole 'two drivers hit the same spot' problem becomes one CPU instruction."
Configurable how? Where does the config live? How is it reloaded?
"PricingStrategy is an interface with one method, calculate(Duration, VehicleSize). Implementations live in their own files — HourlyPricing, DayNightPricing, WeekendSurcharge. The lot holds the strategy by reference, so an admin endpoint can hot-swap it without restarting. Decorator on top — PeakHourSurcharge wraps any strategy and adds 30% between 9–11am."
Below is a minute-by-minute script of an idealised candidate (call her Priya) running the framework on the parking-lot prompt. Read it as the tempo and tone you should aim for — concise, structured, narrating the framework out loud as she builds.
ParkingLot (the registry), Floor (groups spots), Spot (one slot, has size + occupancy), Vehicle (transient, has plate + size), Ticket (issued at entry), Bill (computed at exit). I'm leaving out EntryGate and DisplayBoard — those are interface concerns, not domain entities."ParkingLot composes Floor — composition because floors don't exist outside their lot. Floor composes Spot for the same reason. Spot is abstract because Bike, Car, and Truck spots have different size constraints — that's inheritance because the subclasses truly substitute. PricingStrategy is an interface and the lot holds it by reference (association) — pricing rules change every quarter and I want a new rule to be a new file."PricingStrategy — pricing is the most-likely-to-change axis. Factory on Spot creation — the lot doesn't want to know which subclass to instantiate. Singleton on the lot itself — one per facility. Decorator for surge pricing — wraps any strategy without subclassing it. State pattern is overkill here because parking has only two states (free/occupied)."lot.park(vehicle), lot delegates to allocator, allocator picks a free spot from the floor, calls spot.tryOccupy(vehicle) which is a single CAS. If false, allocator tries the next spot. If true, lot creates a Ticket and returns it."VehicleSize. Then interfaces — PricingStrategy, SpotAllocator. Then abstract Spot with AtomicReference<Vehicle> and tryOccupy(v) doing the CAS. Concrete CarSpot, TruckSpot. Vehicle, Ticket, Bill as records — no boilerplate."ParkingLot service — holds the allocator, the pricing, and a ConcurrentHashMap of open tickets. park() calls allocator, creates a ticket, inserts it. exit() removes the ticket, vacates the spot, runs pricing, returns the bill."main() — instantiate a SimpleAllocator with one CarSpot, an HourlyPricing at ₹40/hr, build the lot, park a vehicle, sleep 3 seconds, exit, print the bill. That's the end-to-end happy path."EvSpot subclass with a chargeRate field. (2) New EvPricing decorator that wraps the existing strategy and adds an energy line. (3) Allocator already returns the right spot for a given size — extend VehicleSize with EV_CAR. Zero edits to ParkingLot — it never sees the difference."openTickets map needs a TicketRepository interface backed by Postgres in production. (2) Reservation flow — that has its own state machine (PENDING → CONFIRMED → EXPIRED) and would use the State pattern. (3) Observability — emit a domain event on every park/exit so analytics can subscribe."Each existing LLD on this site emphasises different framework steps and showcases different patterns. When you're practising, pick a page based on the step you want to drill.
| LLD Page | Steps Emphasised | Patterns Showcased | Best for Practising |
|---|---|---|---|
| Parking Lot | All 5 steps end-to-end | Strategy, Factory, Singleton, State, Decorator, Observer | The canonical resource-allocation problem |
| Vending Machine | Step 4 (State pattern deep dive) | State, Strategy | State machine modelling — when to use State vs enum |
| Notification Service | Steps 3 + 4 (multi-channel fan-out) | Strategy, Factory, Decorator, Observer | Variation along multiple axes (channel, retry, rate-limit) |
| Hotel Management | Steps 1 + 3 (entity-rich domain) | Composite, Strategy, State for booking lifecycle | Domains with many entities and lifecycle states |
| MakeMyTrip | Step 4 (10 patterns mapped to variability) | The full GoF tour applied to a marketplace | Reading "what pattern fits where" at scale |