Low-Level Design (LLD): Writing Code That Future You Won't Hate
Most developers know the sinking feeling of opening a 3,000-line “Manager” or “Helper” file and having no idea what it does or how to change one line without breaking ten others. That is what happens when code gets written without Low-Level Design. In this guide, we demystify LLD with real, relatable examples — showing how to turn messy requirements into clean, testable object models.
👉 Big Picture First: Need to see how your services connect before designing classes? Read our companion guide: High-Level Design (HLD).
What is Low-Level Design, Really?
If High-Level Design (HLD) is the floor plan of an entire apartment building, Low-Level Design (LLD) is the layout of the kitchen: where the refrigerator sits, which way the cabinet doors open, and where the electrical sockets are wired.
In day-to-day engineering, LLD answers the practical questions every developer faces before writing code:
- What classes and objects do we actually need?
- Who is responsible for what (and more importantly, who shouldn't be)?
- Which design pattern makes this code flexible without adding useless complexity?
- What happens when two users click the same button at the exact same millisecond?
A good LLD is essentially a blueprint. It means you or any other developer on your team can sit down and write clean code on Monday morning without arguing over class names or finding out on Thursday that the database design doesn't fit the business logic.
The Classic LLD Exercise: The Parking Lot
Almost every engineer has faced the famous “Design a Parking Lot” interview question. Why? Because it immediately reveals whether you write spaghetti code or clean, modular systems.
Let's break down how an experienced developer models it step-by-step:
Step 1: Start with the Real-World Entities
Close your laptop for a minute and picture a physical parking garage:
- A ParkingLot has multiple Floors.
- Each floor has multiple ParkingSpots (Compact, Large, Handicapped).
- A Vehicle (Car, Motorcycle, Truck) arrives at an entry gate.
- The system assigns a spot and prints a ParkingTicket.
- When leaving, the system calculates the fee based on a PricingStrategy.
classDiagram
class ParkingLot {
-String lotId
-List~ParkingFloor~ floors
+parkVehicle(Vehicle v) ParkingTicket
+checkout(ParkingTicket t) Receipt
}
class ParkingFloor {
-int floorNum
-List~ParkingSpot~ spots
+findAvailableSpot(VehicleType type) ParkingSpot
}
class ParkingSpot {
-String spotId
-SpotType spotType
-boolean isOccupied
-Vehicle parkedVehicle
+assign(Vehicle v)
+vacate()
}
class Vehicle {
<>
-String licensePlate
-VehicleType type
+getType() VehicleType
}
class Car {
}
class Motorcycle {
}
class FeeCalculator {
<>
+calculateFee(Duration duration, SpotType type) decimal
}
class HourlyRateStrategy {
+calculateFee(Duration duration, SpotType type) decimal
}
ParkingLot *-- ParkingFloor
ParkingFloor *-- ParkingSpot
Vehicle <|-- Car
Vehicle <|-- Motorcycle
ParkingSpot o-- Vehicle
ParkingLot --> FeeCalculator
FeeCalculator <|.. HourlyRateStrategy
Why This Class Structure Works
-
Favor Composition Over Giant Inheritance Trees: A
ParkingSpotdoesn't inherit fromVehicle. It holds a vehicle reference. This means when a motorcycle pulls out and another vehicle pulls in tomorrow, you don't have to rebuild the spot object. -
The Strategy Pattern for Pricing: Notice how pricing is handled by a separate
FeeCalculatorinterface. What happens when business management says: “Starting Friday, we want flat-rate $10 pricing for the holiday weekend”? You just create aHolidayFlatRateStrategyclass. You don't have to touch or risk breakingParkingLotorParkingSpot!
Sequence Flow: What Happens When a Car Arrives?
Static class diagrams only tell half the story. The real bugs happen when objects start talking to each other. Here is the step-by-step sequence when a driver pushes the button at the entry barrier:
sequenceDiagram
autonumber
actor Driver as Driver at Entry Gate
participant Gate as Entry Gate Terminal
participant Lot as ParkingLot Coordinator
participant Floor as ParkingFloor (Level 1)
participant Spot as ParkingSpot (#104)
participant TicketService as Ticket Generator
Driver->>Gate: 1. Press "Take Ticket" Button
activate Gate
Gate->>Lot: 2. parkVehicle(Car: "ABC-123")
activate Lot
Lot->>Floor: 3. Is there a free COMPACT spot?
activate Floor
Floor->>Spot: 4. Are you available?
activate Spot
Spot-->>Floor: 5. Yes, Spot #104 is vacant!
deactivate Spot
Floor-->>Lot: 6. Spot #104 reserved
deactivate Floor
Lot->>Spot: 7. Lock and assign Car "ABC-123"
activate Spot
Spot-->>Lot: 8. Confirmed
deactivate Spot
Lot->>TicketService: 9. Issue ticket for Car on Spot #104
activate TicketService
TicketService-->>Gate: 10. Print Ticket (Timestamp: 14:02)
deactivate TicketService
deactivate Lot
Gate-->>Driver: 11. Dispense ticket and raise gate arm
deactivate Gate
Choosing the Right In-Memory Data Structure
A common trap in LLD is using a simple ArrayList for everything. Let's look at why data structure choices make or break your code:
-
Finding a Free Spot: If a garage has 5,000 spots, looping through an array on every arrival is slow. Instead, keep a Min-Heap (PriorityQueue) of free spots ordered by distance to the entrance. Grabbing the nearest available spot is an instant
O(log N)operation. -
Looking Up an Active Ticket: When a driver scans their ticket at checkout, you need instant lookup. Use a
HashMap<TicketId, ParkingTicket>for immediateO(1)retrieval instead of searching through database rows. -
Handling Two Gates at Once: What if two cars arrive at Entry Gate A and Entry Gate B at the exact same millisecond? If your code isn't thread-safe, both gates might assign Spot #104 to two different cars! Use atomic primitives or thread-safe collections like
ConcurrentHashMapto avoid double-booking disasters.
Three Rules to Keep Your Code Clean
-
1. Single Responsibility: A class should do one job well. If your
Carclass is also calculating tax rates and connecting to an SQL database, stop immediately and split it up. -
2. Never Expose Mutable Collections: If a method does
public List<ParkingSpot> getSpots() { return this.spots; }, any rogue piece of code elsewhere can callgetSpots().clear()and wipe out your entire garage. Always return an unmodifiable copy. -
3. Tell, Don't Ask: Instead of asking an object for its internal numbers and doing math on its behalf, tell the object what you want it to do (e.g.,
spot.occupy(car)instead ofif (spot.getStatus() == 0) { spot.setStatus(1); }).
- To master clean class design rules, read SOLID Principles: Building Maintainable Software.
- To understand macro service topologies, read High-Level Design (HLD).
- To learn how component boundaries interact cleanly, read Modularity in Software Architecture.
Final Thoughts
Low-Level Design is where engineering craftsmanship shows. It is not about using fancy patterns for the sake of looking clever; it is about building modular, readable, and thread-safe components that welcome change instead of breaking when the product requirements inevitably shift.