Low-level design
What low-level design is actually asking
Low-level design asks how objects inside one program should share the work: which types exist, which methods they expose, and which object owns each responsibility. For example, a parking lot that starts as one file with a few arrays and if-statements can park a car — until you must add EV spots, two fee rules, and two gates that must never assign the same spot.
At that point the file may still “work,” but it is no longer a design. New requirements edit the same class in five places. LLD is the skill of splitting responsibilities so change lands in the right type instead of rewriting one god class.
What this round grades
Name the use cases
Park, exit, pay — start with verbs you can say out loud.
Name things with identity
Ticket, spot, vehicle — things you can point at later.
Give each verb a home
If every answer is “the Manager,” keep splitting.
Leave a gap for change
Pricing and allocation almost always change next.
The work is splitting responsibilities, not typing syntax
is deciding how objects share work inside one program. You choose which concepts become classes, which actions become methods, and who may mutate which state. A new rule should usually land as a new type, not as one more branch inside the same giant class.
It is not “can you write Java.” Classes and interfaces live in the language track. It is not system design: you are not placing Kafka, Redis, or a load balancer. It is not DSA: you may use a HashMap, but the grade is still the object model, not a clever Big-O trick.
Think of three interview units. DSA asks: how do I compute this efficiently? LLD asks: how should the objects share the work? System design asks: how should many machines share the work? This subject is the middle one. Keep those three talks apart, or you will answer the wrong interview.
Start with verbs you can say out loud: park, exit, pay. Those are . Then name the things you can point at later: ticket, spot, vehicle. Then ask which object performs each verb. If the answer is “the Manager,” keep asking. A is how every leftover responsibility collapses back into one class with a confident-looking name.
Three interviews, three units of work
DSA, LLD, and system design answer different questions
The same product name can show up in all three rounds. The unit of work is what changes. If you bring the wrong unit, you can sound fluent and still miss what the interviewer is grading.
DSA
How do I compute it efficiently?
Find a free index. Sort fees. Walk a graph of floors. The grade is correctness and complexity, not which object owns the ticket.
LLD
How should objects share the work?
Who allocates? Who prices? Who stores? Who must not know about SQL? This page lives here.
System design
How should many machines share the work?
Gates in two cities, a cache of free spots, a queue for receipts. That is a different note.
The next page, HLD versus LLD, compares distributed design (services, stores, failure domains) with in-process design (classes, methods, threads). Read it when you feel Redis creeping into a class diagram.
The public shape is tiny on purpose
Callers do not need twenty methods. They need a they can state in one breath. Everything else is how the object model keeps that contract honest when EV pricing or a second allocation rule arrives tomorrow.
interface ParkingLotService {
Ticket park(Vehicle vehicle);
Receipt exit(TicketId ticketId);
}That is the whole public surface. Park returns a ticket you can hold. Exit consumes that ticket and returns a receipt. If you cannot write those two signatures in the first five minutes, you are still guessing at the problem. The approach page is about getting this contract agreed before you type the rest.
God-object file
ParkingLotManager does every job
This compiles and parks a car. It is still a weak design. Pricing, storage, and notifications all sit in the same class, so the next requirement edits all of them at once.
class ParkingLotManager {
List<Spot> spots = new ArrayList<>();
Map<String, Ticket> tickets = new HashMap<>();
Ticket park(Vehicle v) {
for (Spot s : spots) {
if (s.free) {
s.free = false;
s.plate = v.plate;
Ticket t = new Ticket(UUID.randomUUID().toString(), v, s, now());
tickets.put(t.id, t);
sms.notify("parked " + v.plate);
return t;
}
}
throw new RuntimeException("full");
}
Receipt exit(String id) {
Ticket t = tickets.get(id);
long hours = (now() - t.in) / 3_600_000;
double fee = t.vehicle.type.equals("EV") ? hours * 30 : hours * 20;
t.spot.free = true;
return new Receipt(id, fee);
}
}Split responsibilities
Each verb has a home
The lot still orchestrates. It does not invent fees, walk every spot itself, or send SMS. Occupancy lives on the spot so the cannot leak into five files.
class ParkingLot implements ParkingLotService {
private final SpotChooser chooser;
private final TicketBook tickets;
private final Pricing pricing;
private final Clock clock;
public Ticket park(Vehicle vehicle) {
ParkingSpot spot = chooser.choose(vehicle)
.orElseThrow(LotFullException::new);
spot.occupy(vehicle);
return tickets.issue(vehicle, spot, clock.instant());
}
public Receipt exit(TicketId ticketId) {
Ticket ticket = tickets.require(ticketId);
Money fee = pricing.fee(ticket, clock.instant());
ticket.spot().vacate();
tickets.close(ticket);
return new Receipt(ticket.id(), fee);
}
}
class ParkingSpot {
private Vehicle occupant;
void occupy(Vehicle vehicle) {
if (occupant != null) {
throw new IllegalStateException("spot already held");
}
occupant = vehicle;
}
void vacate() {
occupant = null;
}
}Leave a gap
Pricing and allocation will change. Plan for that without naming a pattern yet.
Interviewers almost always add a second vehicle type or a second fee rule. If those decisions live in a switch inside exit, the lot gets edited in five places. If they live behind small types, you add a class and leave the lot unchanged.
A is not “I will use Strategy.” A seam is a named job that can vary: choose a free spot, compute a fee. You may later plug a strategy into that job. First you must know who calls it. Pattern names without owners are decoration.
interface SpotChooser {
Optional<ParkingSpot> choose(Vehicle vehicle);
}
interface Pricing {
Money fee(Ticket ticket, Instant exitTime);
}
// Duration.toHours() truncates; bill by ceiling from seconds (started hour).
static long billableHours(Duration stay) {
long seconds = stay.getSeconds();
if (seconds < 0) throw new IllegalArgumentException("negative stay");
return Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
}
class HourlyByVehicleType implements Pricing {
public Money fee(Ticket ticket, Instant exitTime) {
Duration stay = Duration.between(ticket.enteredAt(), exitTime);
long hours = billableHours(stay);
long paise = hours * ticket.vehicle().type().hourlyPaise();
return Money.inr(paise);
}
}Duration.toHours() truncates toward zero — 1h59m becomes 1. Hourly parking that bills a started hour must ceil from seconds (or nanos), then apply the one-hour minimum.
A working order you can say out loud
You do not need a perfect class diagram before you talk. You need a sequence that keeps responsibilities from collapsing back into one manager class.
- 1
Name the use cases
Park, exit, pay. If two gates exist, say that now. If payments are out of scope, say that too.
- 2
Name the things with identity
Ticket, Spot, Vehicle, Floor. Things you can point at later usually become entities.
- 3
Name the values
Money, VehicleType, Duration. Twenty rupees is not “that twenty.” Keep them small and hard to mix up.
- 4
Give each verb a home
Who allocates? Who prices? Who stores? Who protects occupancy? If the answer is still “the Manager,” keep going.
- 5
Protect one invariant
A spot is held by at most one vehicle. Put that rule on the spot, not in a comment on the service.
- 6
Leave the seam
Chooser and pricing should be replaceable. You do not have to implement three variants today.
Walk one happy path before you draw twenty boxes
Candidates freeze when they try to recall “the” class diagram. Walk the car instead. A vehicle arrives. Someone chooses a spot. The spot becomes occupied. A ticket is issued. Later the ticket is loaded, a fee is computed, and the spot is freed. If you can narrate that chain, the classes fall out of the verbs.
void happyPath() {
ParkingLot lot = new ParkingLot(
List.of(floorWith(2, SpotType.CAR)),
new NearestEmptyChooser(),
new InMemoryTicketBook(),
new HourlyByVehicleType(),
Clock.systemUTC()
);
Ticket ticket = lot.park(new Car("KA-01-AB-1234"));
assert ticket.spot().isOccupied();
assert ticket.vehicle().plate().equals("KA-01-AB-1234");
Receipt receipt = lot.exit(ticket.id());
assert receipt.fee().paise() > 0;
assert ticket.spot().isFree();
}Even a crude test like this keeps you honest when you split the Manager apart. If occupy and vacate are not on the spot, the test can still pass while two gates hand out the same stall. That race belongs in race conditions, but you should name the shared state today.
Mistakes that look like design
Starting with a pattern name
Building a ParkingLotManager
Optimizing a HashMap before the model exists
Dropping Redis into the class list
How this note connects to the rest of the track
This page is the door. The next four fundamentals notes turn the same parking lot into a method, a class list, and a scorecard. Domain modeling then asks who allocates. SOLID and GRASP give names to jobs you already placed. Patterns come last, as tools, not as the starting point.
HLD versus LLD
Distributed services, or objects in one program?
How to run the round
Talk, agree the API, then code. Forty-five minutes.
Requirements to classes
Underline nouns and verbs until the Manager disappears.
What they actually grade
Model, jobs, API, change, two actors, talking.
Domain modeling
Who should allocate a spot — and who must not?
Parking lot case
The same problem, fully built as machine coding.
Try this before you move on
- Write only the
ParkingLotServiceinterface and the names of four collaborators. Do not implement yet. Can you defend each name? - Add EV spots as a follow-up. Which files change if pricing lives in
exit? Which files change if pricing is its own type? - Two gates call
parkat once. Where is the shared state? What must not split? You do not need a lock in minute five. You do need the sentence.
Takeaway
Low-level design is splitting work among objects so a new rule does not rewrite one giant class. Keep the public API tiny. Put each verb in a home. Protect the one rule that must stay true. Leave a gap where change always arrives. The next page shows the same parking lot at two altitudes — distributed HLD versus in-process LLD — and why mixing those two jobs is how strong candidates still fail the round.