How to write clean code in LLD interviews
Write readable interview code with meaningful names, small focused methods, clear structure, and comments that explain decisions, not syntax.
You have twenty minutes left in the interview. Your parking lot system works, but the reviewer now has to navigate a 45-line parkVehicle() method, a variable called x, and a comment that says // increment counter above counter++. The code runs, but its intent is harder to verify.
Clean code is not about perfection. It is about making intent visible: how the code is named, where responsibilities live, how failure is represented, and what a future change would touch. In a bounded LLD round, those choices make a working design easier to discuss and maintain.
This guide gives you practical rules with before-and-after Java examples.
TL;DR
The purpose of this article is to keep interview code readable while preserving time for the required behavior.
- 30-second idea: use domain names, small cohesive methods, explicit error contracts, and comments only for decisions that the code cannot explain.
- Mental model:
name the intent β isolate one responsibility β protect invariants β make failure explicit β check the change surface. - Repeatable process: name each class and method as you introduce it, extract a method when a block has its own reason to change, validate at boundaries, and review the public API before the final trace.
- Adapt to constraints: treat line counts, package depth, and abstraction as heuristics. Preserve clarity and correctness first; simplify optional structure when time or scope is tight.
Why clean code matters
Three reasons. First, readability. The interviewer is not running your code, they are reading it. If they have to squint at a variable name or re-read a method three times, you are burning their goodwill.
Second, maintainability. Clean code shows you think about the engineer who reads this six months from now. In a real codebase, that engineer is often you.
Third, communication. Code quality gives the reviewer evidence about responsibilities, invariants, and likely change points. A correct design can still be difficult to assess when it looks like an unexamined first draft.
In an interview, small naming and structure habits reduce review friction, but they still need to be balanced against the time required to finish the core behavior.
Naming things
Naming is the most visible clean code skill. The interviewer sees your names before they understand your logic. Bad names force re-reading. Good names make the code read like prose.
Classes: nouns, Methods: verbs
| Type | Bad | Good | Why |
|---|---|---|---|
| Class | Manager | ParkingLotService | What does it manage? Be specific. |
| Class | Helper | FareCalculator | Helpers are a code smell. Name the job. |
| Method | spot() | assignSpot() | What about the spot? Find it? Create it? |
| Method | check() | isSpotAvailable() | Boolean methods start with is, has, can. |
| Method | do() | calculateFare() | "Do" is meaningless. Name the action. |
Variables: descriptive, not abbreviated
The time saved by typing cnt instead of counter is usually smaller than the time it takes a reader to reconstruct the meaning. In an interview, the reader must understand the code while also evaluating the design.
// β Cryptic
int n = spots.size();
for (int i = 0; i < n; i++) {
if (spots.get(i).getT() == 0) { ... }
}
// β
Descriptive
int totalSpots = spots.size();
for (int i = 0; i < totalSpots; i++) {
if (spots.get(i).getType() == SpotType.COMPACT) { ... }
}
Constants: UPPER_SNAKE_CASE
// β Magic numbers buried in logic
if (duration > 24) { fare = 500; }
// β
Named constants
private static final int MAX_PARKING_HOURS = 24;
private static final int FLAT_DAILY_RATE = 500;
if (duration > MAX_PARKING_HOURS) { fare = FLAT_DAILY_RATE; }
Interview tip: name things as you go
Avoid naming variables temp with the intention of renaming them later. A useful name forces you to decide what the value represents; if you cannot name it, pause long enough to clarify that responsibility.
The rule of thumb: if someone can understand what a variable holds, what a method does, and what a class represents without reading the implementation, your names are good.
Small methods
Long methods are the most common interview code smell. A 40-line method forces the interviewer to hold the entire thing in working memory. A 10-line method is self-contained, testable, and readable.
The rule: one responsibility per method
Each method should do exactly one thing. If you find yourself writing a comment to separate "sections" inside a method, those sections should be separate methods.
Before vs after
// β One method does validation, search, assignment, and logging.
public ParkingTicket parkVehicle(Vehicle vehicle) {
if (vehicle == null) throw new IllegalArgumentException("Vehicle is null");
if (vehicle.getLicensePlate() == null || vehicle.getLicensePlate().isBlank())
throw new IllegalArgumentException("No license plate");
ParkingSpot found = null;
for (ParkingSpot spot : spots) {
if (!spot.isOccupied() && spot.getType().fits(vehicle.getType())) {
found = spot; break;
}
}
if (found == null) throw new NoAvailableSpotException(vehicle.getType());
found.setOccupied(true);
found.setVehicle(vehicle);
ParkingTicket ticket = new ParkingTicket(
UUID.randomUUID().toString(), vehicle, found, Instant.now());
activeTickets.put(ticket.getId(), ticket);
System.out.println("Parked " + vehicle.getLicensePlate() + " in " + found.getId());
return ticket;
}
// β
Each method has one job. The orchestrator reads like a checklist.
public ParkingTicket parkVehicle(Vehicle vehicle) {
validateVehicle(vehicle);
ParkingSpot spot = findAvailableSpot(vehicle.getType());
return assignVehicleToSpot(vehicle, spot);
}
private void validateVehicle(Vehicle vehicle) {
if (vehicle == null) throw new IllegalArgumentException("Vehicle is null");
if (vehicle.getLicensePlate() == null || vehicle.getLicensePlate().isBlank())
throw new IllegalArgumentException("No license plate");
}
private ParkingSpot findAvailableSpot(VehicleType type) {
return spots.stream()
.filter(s -> !s.isOccupied() && s.getType().fits(type))
.findFirst()
.orElseThrow(() -> new NoAvailableSpotException(type));
}
Use 5 to 15 lines as a heuristic, not a rule. If a method grows past roughly 20 lines or contains multiple distinct phases, look for an extraction opportunity while preserving a readable flow.
Meaningful structure
Clean code is not just clean methods. It is clean organization. Where you put classes and how they relate sends signals to the interviewer.
Package organization
In an LLD interview, you do not need Maven multi-module builds. But grouping your classes by domain responsibility shows architectural thinking.
Three rules for package structure in interviews:
- model/ holds domain entities and enums. No logic beyond validation.
- service/ holds orchestration. Services call strategies, repositories, validators.
- strategy/ holds pluggable algorithms. This is where design patterns live.
You do not need physical folders in an interview. Prefix class names or add a comment such as // model when that is enough to make the boundaries visible.
Class cohesion
Every class should have a single reason to change. If your ParkingLotService calculates fares, sends notifications, and manages spots, it needs to be split.
The test: describe what the class does in one sentence without the word "and." If you need "and," you need two classes.
Comments done right
Most interview code has too many comments, and they say the wrong thing. Comments explain why, not what. The code already tells you what it does.
Bad comments (remove these)
// β Restates the code
int count = 0; // initialize count to zero
count++; // increment count
// β Commented-out code (use version control)
// public void oldMethod() { ... }
Good comments (keep these)
// β
Explains a business rule
// Cap at 24 hours: billing contract charges flat daily rate beyond one day.
if (durationHours > MAX_PARKING_HOURS) {
return FLAT_DAILY_RATE;
}
// β
Explains a non-obvious technical decision
// ConcurrentHashMap: multiple kiosk threads assign spots simultaneously.
private final Map<String, ParkingSpot> spotMap = new ConcurrentHashMap<>();
The over-commenting trap
Candidates sometimes add comments to look thorough. It backfires. Every unnecessary comment is noise the interviewer has to skip. Write self-documenting code with good names and reserve comments for genuine surprises.
The best code needs few comments because the names and structure make intent obvious. A missing comment is often less harmful than one that contradicts the code or restates it.
Error handling
How you handle errors tells the interviewer how you think about edge cases. "What happens when the parking lot is full?" should not crash the program or return null silently.
Choose an explicit failure contract
Choose an explicit failure contract. Exceptions are useful for unexpected or exceptional domain failures; Optional or a result type can be clearer for an expected absence. Avoid ambiguous sentinel values such as -1 and unexplained null returns.
// β Caller must remember to check for null
public ParkingSpot findSpot(VehicleType type) {
for (ParkingSpot spot : spots) {
if (!spot.isOccupied() && spot.fits(type)) return spot;
}
return null;
}
// β
Failure is impossible to ignore
public ParkingSpot findSpot(VehicleType type) {
return spots.stream()
.filter(s -> !s.isOccupied() && s.fits(type))
.findFirst()
.orElseThrow(() -> new NoAvailableSpotException(type));
}
Fail fast with custom exceptions
Validate inputs at the boundary. Do not let bad data propagate three layers deep before a NullPointerException surfaces.
// β
Validate at the entry point
public ParkingTicket parkVehicle(Vehicle vehicle) {
Objects.requireNonNull(vehicle, "Vehicle must not be null");
Objects.requireNonNull(vehicle.getLicensePlate(), "License plate required");
}
// β
Domain-specific exception: instantly understandable
public class NoAvailableSpotException extends RuntimeException {
private final VehicleType vehicleType;
public NoAvailableSpotException(VehicleType vehicleType) {
super("No available spot for vehicle type: " + vehicleType);
this.vehicleType = vehicleType;
}
}
For a small LLD problem, one or two focused custom exceptions are usually enough. Build a larger hierarchy only when the requirements or callers need distinct handling.
Trade-offs and adapting to constraints
Clean code is a set of decisions, not a demand for maximum abstraction.
- Time pressure: keep the public API and core flow readable first. Defer package rearrangement, generated boilerplate, and optional abstractions.
- Method size: use extraction when it clarifies a responsibility or makes a rule testable; do not split a cohesive operation into tiny methods that hide the flow.
- Error handling: choose exceptions,
Optional, or a result type based on whether absence is expected and how callers should respond. Keep the contract consistent. - Comments: explain business rules, invariants, and non-obvious technical choices. If a name or method boundary can explain it, prefer that over a comment.
- Abstraction: introduce an interface at a real variation point, such as when pricing rules may vary by customer type. A direct implementation is often clearer when there is only one stable behavior.
- Performance and safety: defensive copies and immutable values can improve correctness but may add allocation or copying cost. Mention the trade-off when the collection is large or the path is hot.
Implementation: before and after
This is the full picture. A messy order service refactored into clean code. Study the before, understand what is wrong, then see how the after addresses every issue.
// β This class does too much: validation, pricing, payment, notification.
// Method names are vague. Variables are abbreviated. Magic numbers everywhere.
public class OrderProcessor {
private List<Object[]> orders = new ArrayList<>();
public int process(String cust, List<Object[]> items, String type) {
// validate
if (cust == null || cust.isEmpty()) return -1;
if (items == null || items.size() == 0) return -1;
// calc total
double t = 0;
for (Object[] item : items) {
double p = (double) item[0]; // price
int q = (int) item[1]; // qty
t += p * q;
}
// apply discount
if (type.equals("VIP")) {
t = t * 0.9; // 10% off
} else if (type.equals("EMPLOYEE")) {
t = t * 0.7; // 30% off
}
// tax
t = t * 1.18; // 18% tax
// save
Object[] order = new Object[]{cust, items, t, type};
orders.add(order);
// notify
System.out.println("Order placed for " + cust + " total: " + t);
return orders.size() - 1; // return index as "id"
}
}Look at what changed:
| Messy (before) | Clean (after) | Principle |
|---|---|---|
Object[] for everything | OrderItem record with types | Type safety, named fields |
String type with "VIP" | CustomerType enum | No magic strings |
double t | BigDecimal subtotal | Descriptive names, correct type for money |
| One 35-line method | Five focused classes | Single responsibility |
return -1 for errors | Exceptions with messages | Fail fast, clear contracts |
0.9 magic number | VIP_RATE constant | Named constants |
| Inline discount logic | DiscountStrategy interface | Open for extension |
Common mistakes
These are common patterns that make interview code harder to read or change.
1. Over-commenting
Delete every comment that restates the code. int count = 0; // initialize count to zero adds nothing. Comments are for the why, not the what.
2. Long methods
If a method no longer fits comfortably in the editor or requires holding several responsibilities in memory, look for an extraction. Keep the resulting methods cohesive rather than splitting mechanically.
3. Clever code
// β Takes 30 seconds to parse
return t == null ? -1 : t.equals("VIP") ? p * 0.9 : t.equals("EMP") ? p * 0.7 : p;
// β
Takes 2 seconds to understand
switch (customerType) {
case VIP -> subtotal.multiply(VIP_DISCOUNT);
case EMPLOYEE -> subtotal.multiply(EMPLOYEE_DISCOUNT);
default -> subtotal;
}
Clever code often optimizes for brevity at the reader's expense. Prefer the version whose intent is obvious from a quick read.
4. Returning null
An unexplained null return is easy to misuse. Return an empty collection, throw a meaningful exception, or use Optional/a result type according to the API contract.
// β Caller must check for null
public ParkingSpot findSpot(VehicleType type) { /* ... might return null */ }
// β
Optional makes absence explicit
public Optional<ParkingSpot> findSpot(VehicleType type) {
return spots.stream()
.filter(s -> !s.isOccupied() && s.fits(type))
.findFirst();
}
Interview tip: fix as you go
If you spot messy code mid-interview, fix the smallest problem that improves clarity without losing the core flow. Saying "let me extract this validation step" makes the reason for the change visible.
30-second and 5-minute explanations
30-second explanation
"Clean code makes intent and change boundaries visible. Use domain names, keep each method focused, protect invariants at the boundary, and make failure behavior explicit. Add an abstraction only for a real variation point, and use comments for business rules or non-obvious decisions."
5-minute explanation
Start with the public API and ask whether each name explains its role. Walk through one method: separate validation, lookup, mutation, and side effects only when they have distinct responsibilities. Use the order example to show how typed values, immutable records, a pricing strategy, and a focused service replace magic strings, arrays, and a god method. Then discuss the trade-offs: method-length rules are heuristics, exceptions are not the only failure contract, and defensive copies or abstractions should be justified by correctness and likely change.
The clean code checklist
Use this as a mental checklist in your next LLD interview. It takes ten seconds to scan before you start coding.
Test Your Understanding
Q1. What is the first refactoring question for a long method?
Answer: Which parts have a distinct responsibility or reason to change? Extract those parts only when the resulting flow is clearer.
Q2. When should a comment remain?
Answer: When it explains a business rule, invariant, or non-obvious technical decision that names and structure cannot express.
Q3. What is a safer alternative to an unexplained null return?
Answer: Use an empty collection, Optional, a result type, or a meaningful exception according to the API contract.
Q4. When is an interface worth adding in a small LLD solution?
Answer: When a real behavior varies or must be independently tested; otherwise, direct code may be clearer.
Q5. What should be protected when time is short?
Answer: Names, invariants, the public contract, and the core flow. Defer optional structure and cosmetic cleanup.
Quick recap
- Names are your first impression. Classes are nouns, methods are verbs, variables are descriptive, constants are UPPER_SNAKE_CASE.
- Small methods make responsibilities easier to inspect. One job per method. Use 5 to 15 lines as a heuristic, and let the orchestrator read like a checklist.
- Comments explain decisions, not syntax. If a comment restates the code, delete it. If it explains a business rule or a non-obvious tradeoff, keep it.
- Make failure explicit. Validate inputs at the boundary. Use custom exceptions for domain-specific failures, or
Optional/a result type for expected absence. Avoid unexplainednullfor "not found." - Structure shows architectural thinking. Group classes by responsibility (model, service, strategy). Each class has one reason to change.
- Avoid cleverness. Straightforward code is easier to review. If a line needs re-reading, look for a clearer name, method boundary, or expression.
- Clean code is a habit, not a phase. Keep the core readable as you build, then use the remaining time for the highest-value cleanup.
Related concepts
- OOD interview approach places clean-code decisions inside the broader design process.
- Machine coding approach applies the same readability principles when the code must compile and run under time pressure.
- Design principles provides the underlying cohesion, coupling, and responsibility concepts.
Related Articles
A step-by-step framework for object-oriented design interviews, from clarifying requirements to implementing clean, extensible code in a bounded session.
A practical framework for machine coding rounds, from reading the problem to delivering compilable, well-structured code in a timed session.