Definition
- Clarify requirements – pin down functional needs, constraints, and explicit out-of-scope items before designing anything.
- Identify actors – find the people or external systems that interact with the system; they reveal the entry points and operations.
- Extract entities – scan the requirements for important nouns; these become your classes with their key attributes.
- Assign responsibilities – each class does one job (Single Responsibility Principle); no God class.
- Define relationships – composition, aggregation, or inheritance, plus interfaces where you expect variation.
- Apply patterns judiciously – use a pattern only when it solves a genuine problem, never for show.
Suppose National Bank of India asks you to design a safe-deposit-locker system. You do not start coding. First you ask who uses it: customers like Rajesh, bank staff, and an auditor. Then you list the important things: Locker, Vault, Customer, Booking, Payment. Then you give each thing one clear job: the Locker knows its size and status, the Vault manages lockers, the Booking links a customer to a locker for a period. Then you decide how they connect. Only then, if a real need appears, you reach for a pattern.
Understanding the Concepts
What is a low-level design question?
A low-level design (LLD) question asks you to produce the class-level design of a bounded system: the entities, their fields and methods, the relationships between them, the interfaces, and the key patterns. The goal is a cohesive, extensible object model, not a premature framework.
In an interview, LLD questions are deliberately vague. The interviewer wants to see whether you ask clarifying questions, how you discover entities, and whether you apply patterns appropriately or force them.
How do you clarify requirements and scope?
The first step is to ask scoping questions. For a parking lot you would ask: how many levels, what vehicle types, is payment included, is it single-threaded or concurrent. Writing down scope prevents you from designing the wrong thing.
package com.bank.lld;
/**
* Requirements captured for the Safe-Deposit Locker system
* at National Bank of India.
*
* Functional:
* - Rajesh (customer) can reserve a locker by size.
* - Staff can check in / check out a customer.
* - Payment is charged by duration.
*
* Constraints:
* - Three locker sizes: SMALL, MEDIUM, LARGE.
* - Maximum one active booking per customer.
* - Concurrent access: multiple staff at multiple branches.
*
* Out of scope:
* - Online booking portal (only in-branch).
* - Insurance on locker contents.
*/
public class LockerSystemRequirements {
// This class documents the scoping conversation.
}
How do you identify actors?
Actors are the people or external systems that interact with the system. For the locker system, they are the Customer, the Bank Staff, and the Audit System. Actors reveal the entry points and the operations the system must expose, which later become your public methods.
Actors: Customer (Rajesh, Priya) → reserve locker, release locker Bank Staff → check in, check out, view occupancy Audit System → generate usage reports
How do you extract core entities?
Read the requirements and underline the important nouns. These become your classes. For the locker system: Locker, LockerVault, Customer, Booking, Payment.
package com.bank.model;
import java.math.BigDecimal;
public class Locker {
private final String lockerId;
private final LockerSize size;
private boolean occupied;
public Locker(String lockerId, LockerSize size) {
this.lockerId = lockerId;
this.size = size;
this.occupied = false;
}
public boolean isOccupied() { return occupied; }
public void occupy() {
if (occupied) throw new IllegalStateException(
"Locker " + lockerId + " is already occupied");
this.occupied = true;
}
public void release() {
if (!occupied) throw new IllegalStateException(
"Locker " + lockerId + " is already free");
this.occupied = false;
}
public String getLockerId() { return lockerId; }
public LockerSize getSize() { return size; }
}
package com.bank.model;
public enum LockerSize {
SMALL, MEDIUM, LARGE
}
Each entity gets its key attributes (fields) and its own state transitions. The Locker decides how it becomes occupied; the orchestrator does not reach in and flip a flag.
How do you assign single responsibilities?
Each class should do one job, following the Single Responsibility Principle.
Lockertracks its own state (size, occupied).LockerVaultmanages a collection of lockers and finds a free one.Bookinglinks a customer to a locker for a period.BookingServiceorchestrates the reservation flow.PricingStrategycomputes the fee.
Resist the temptation to create a God class that manages lockers, computes prices, and handles bookings. Split responsibilities so each class has one clear reason to change.
package com.bank.model;
import java.util.List;
import java.util.Optional;
public class LockerVault {
private final String vaultId;
private final List<Locker> lockers;
public LockerVault(String vaultId, List<Locker> lockers) {
this.vaultId = vaultId;
this.lockers = lockers;
}
public Optional<Locker> findFreeLocker(LockerSize size) {
return lockers.stream()
.filter(l -> l.getSize() == size && !l.isOccupied())
.findFirst();
}
}
How do you define relationships and interfaces?
Decide how entities relate:
- Composition (owns-and-dies-together): a
LockerVaultowns itsLockerobjects. If the vault is removed, the lockers go with it. - Aggregation (has-but-independent): a
Bookingreferences aCustomer, but the customer exists independently. - Inheritance (genuine is-a): use only when the relationship is truly is-a, such as
SavingsAccountis-aAccount. - Interfaces: introduce where you expect variation, such as a
PricingStrategyfor different fee tiers.
package com.bank.service;
import java.math.BigDecimal;
public interface PricingStrategy {
BigDecimal computeFee(long hoursUsed);
}
package com.bank.service;
import java.math.BigDecimal;
public class HourlyPricingStrategy implements PricingStrategy {
private final BigDecimal hourlyRate;
public HourlyPricingStrategy(BigDecimal hourlyRate) {
this.hourlyRate = hourlyRate;
}
@Override
public BigDecimal computeFee(long hoursUsed) {
return hourlyRate.multiply(BigDecimal.valueOf(hoursUsed));
}
}
How do you apply design patterns appropriately?
Apply patterns only where they solve a genuine problem. This is the maturity signal interviewers look for.
- Strategy for pricing that genuinely varies by tier.
- Factory if object creation is complex and centralised.
- State for an entity with a real lifecycle (booking states).
- Observer if several things must react to an event.
Do not force patterns. Adding a pattern that solves no real problem is over-engineering and a red flag in an interview.
package com.bank.service;
import com.bank.model.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class BookingService {
private final LockerVault vault;
private final PricingStrategy pricing;
public BookingService(LockerVault vault, PricingStrategy pricing) {
this.vault = vault;
this.pricing = pricing;
}
public Booking reserve(Customer customer, LockerSize size) {
Locker locker = vault.findFreeLocker(size)
.orElseThrow(() -> new IllegalStateException(
"No free " + size + " locker available"));
locker.occupy();
return new Booking(customer, locker, LocalDateTime.now());
}
public BigDecimal release(Booking booking) {
booking.getLocker().release();
long hours = java.time.Duration.between(
booking.getStartTime(), LocalDateTime.now()).toHours();
return pricing.computeFee(Math.max(1, hours));
}
}
How do you verify the design?
Always trace a concrete scenario end to end. This exposes missing methods or awkward relationships before the interviewer finds them.
Scenario: Rajesh reserves a MEDIUM locker 1. BookingService.reserve(rajesh, MEDIUM) 2. LockerVault.findFreeLocker(MEDIUM) → Locker L-042 3. Locker L-042.occupy() 4. Create Booking(rajesh, L-042, now) 5. Return Booking to caller Scenario: Rajesh releases the locker after 3 hours 1. BookingService.release(booking) 2. Locker L-042.release() 3. PricingStrategy.computeFee(3) → 150.00 4. Return fee to caller
What about concurrency?
If the problem implies concurrent access, raise it explicitly. Assigning a locker in a busy branch needs synchronisation or atomic operations so two staff members do not assign the same locker to different customers.
// Thread-safe locker assignment
public synchronized Optional<Locker> findAndOccupy(LockerSize size) {
Optional<Locker> locker = findFreeLocker(size);
locker.ifPresent(Locker::occupy);
return locker;
}
What should a senior engineer know?
- Let entities own their state transitions. The Locker should decide how it becomes occupied, not have the orchestrator reach in and flip a flag.
- Design for the follow-up. Interviewers will ask "how would you add electric-vehicle charging spots?" or "how would you add a new locker size?" A good model answers that by adding a subtype or a new strategy, not by rewriting existing classes (Open/Closed Principle).
- Composition over inheritance. Default to composition. Use inheritance only for genuine is-a relationships.
- Diagram before code. In an interview, sketch the class diagram with relationships before writing any code. It communicates the design faster and is easier to revise.
What are common interview follow-up questions?
- How would you extend this design for a new locker size?
- Where would you use the Observer pattern in this system?
- How would you handle concurrent locker assignment?
- How would you add a cancellation policy?
- Draw the class diagram with relationships.
Visual Overview
LLD Approach: Step-by-Step
──────────────────────────
1. Clarify "How many sizes? Payment included?"
│
▼
2. Actors Customer, Staff, Audit System
│
▼
3. Entities Locker, Vault, Customer, Booking, Payment
│
▼
4. Responsibilities Each class: one job
│
▼
5. Relationships Vault ──has-many──→ Locker
Booking ──refs──→ Customer, Locker
│
▼
6. Patterns Strategy for pricing (only if it varies)
│
▼
7. Verify Trace scenario end to end
┌──────────────────────────────────────────┐
│ BookingService │
│ reserve(customer, size) │
│ release(booking) │
└────────┬──────────────┬──────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ LockerVault │ │ PricingStrategy │
│ findFree() │ │ computeFee() │
└──────┬───────┘ └──────────────────┘
│
▼
┌──────────────┐
│ Locker │
│ occupy() │
│ release() │
└──────────────┘
Key Takeaway
When you get a low-level design question, follow a disciplined sequence rather than jumping into code. First clarify requirements and scope, because these questions are deliberately vague and the interviewer wants to see you ask about sizes, limits, payment, and concurrency. Writing down scope stops you from designing the wrong thing.
Then identify the actors: the people or systems that interact with the system. Actors reveal the operations the system must expose. Next extract the core entities by underlining the important nouns in the requirements. These become your classes, and you give each its key fields. After that, assign each class a single responsibility, resisting the God class. The locker tracks its own state, the vault manages lockers, the booking service orchestrates the flow, and a separate pricing strategy handles money.
Define relationships carefully: composition for owns-and-dies-together, aggregation for has-but-independent, inheritance only for a genuine is-a, and favour composition over inheritance as the default. Introduce interfaces where you genuinely expect variation, such as a pricing strategy or a vehicle abstraction. Only after the model is solid do you apply design patterns, and crucially only where they solve a real problem. Forcing patterns that solve nothing is over-engineering and a red flag.
Always finish by tracing a concrete scenario end to end to expose missing methods. Think about concurrency where the problem implies it so two requests cannot grab the same resource. Make sure the model answers the inevitable follow-up question about extension by adding a new subtype or strategy rather than rewriting existing classes, which is the Open/Closed Principle in action.
Remember one sentence: clarify, find actors, extract nouns, assign responsibilities, define relationships, add patterns only where earned, and trace a scenario to verify.
---