A 12-step algorithm for any Low-Level Design interview — requirements, entity identification, UML, folder structure, code, concurrency, patterns, SOLID, edge cases, logging, and scaling. Follow every step in order. Call out each step by name. This is the playbook, not a reference to skim.
ByAbhishek GoyalFormat 12 sequential stepsTotal time ~25–35 min in-interviewStack Java
The 12 steps
This is a procedure, not a topic list — run it top to bottom, in an interview, out loud. Every step has a line to say before you start it, so the interviewer always knows what phase you're in and why. Skipping a step is the single most common way this kind of interview goes sideways: not because the code was wrong, but because the interviewer couldn't follow the reasoning.
01Clarify Requirements 2–3 min
"Before jumping into implementation, let me clarify both functional and non-functional requirements."
Fig. 1
Functional vs non-functional requirements feed different design decisions
Both feed the same design — but they feed different parts of it. This split is why the two get gathered separately, in this order.
Part A — Functional Requirements (What the system DOES)
"First, let me understand the core use cases."
Ask:
Who are the actors? (User, Admin, Driver, System, Guest)
What actions can each actor perform?
What is the primary happy path end to end?
Are there secondary use cases? (cancel, refund, search, review)
Any features that are explicitly out of scope?
Write these as: "The system shall allow [actor] to [action]."
Example output
Functional Requirements:
- User can browse and search items
- User can add items to cart
- User can place an order
- User can cancel an order (only if status = PENDING)
- User can view order history
- Admin can update inventory
- System sends notification on order confirmation
Part B — Non-Functional Requirements (How the system BEHAVES)
"Now let me clarify the quality constraints."
Area
Questions to ask
Scale
How many users? Expected RPS? Read-heavy or write-heavy?
Concurrency
Can multiple users/threads access the same resource simultaneously?
Consistency
Strong or eventual consistency? (e.g., seat booking must be strongly consistent)
Latency
Any SLA? Response time target? (e.g., < 200ms)
Availability
99.9% uptime? Can the system go down briefly?
Durability
In-memory OK or must data survive restart?
Idempotency
Should the same request twice produce the same result?
Extensibility
Will new types / strategies be added later? (drives pattern choice)
Failure Handling
What happens on partial failure? Retry? Rollback? Compensate?
Scope
Single JVM or distributed? External DB or in-memory?
Example output
Non-Functional Requirements:
- In-memory storage (single JVM, no external DB)
- Concurrent access expected -- thread safety required
- Order placement must be idempotent (same requestId = same result)
- Payment strategy must be extensible (Credit, UPI, Wallet)
- Cancellation only allowed in PENDING state (state validation required)
Output of Step 1 — state these aloud before moving on
"Based on my understanding:
Functional:
1. User can place, cancel, and view orders
2. Admin can manage inventory
3. System notifies user on status change
Non-Functional:
1. In-memory, single JVM
2. Concurrent access -- I'll use ConcurrentHashMap + locks
3. Idempotent order placement -- requestId dedup
4. Payment is extensible -- I'll use Strategy pattern
5. Order has lifecycle -- I'll use State pattern
Let me proceed with these assumptions unless you'd like to change anything."
Why this matters
Functional requirements decide your entities, methods, and relationships. Non-functional requirements decide your data structures, design patterns, and concurrency approach. Every pattern and tool choice you make in Steps 6–8 traces back to something you wrote down here — that traceability is exactly what a senior-level answer sounds like.
The Core Skill
§The Entity Identification Framework
This is the core skill most candidates struggle with. It's not intuition — it's a repeatable algorithm. Run these four steps on any problem statement and the entities fall out.
Step 1 — Noun Extraction Filter
Read the problem statement and underline every noun. Then apply this three-question filter to each one:
Question
If YES →
If NO →
Does it have its own identity (unique ID)?
Strong Entity candidate
Might be a value object / field
Does it have state that changes over time?
Definite Entity
Might be just an enum / constant
Does it have behavior (does things or things happen to it)?
Needs its own class
Might be a field on another entity
Step 2 — The 4 Entity Archetypes
Every entity in any LLD problem falls into one of these four buckets.
Fig. 2
The four entity archetypes
The Coordinator usually references a collection of Resources and creates Transactions when an Actor acts — the four archetypes aren't independent, they interlock.
Step 3 — Relationship Identification
After finding entities, ask these three questions for every pair.
1. Has-A or Is-A?
ParkingLot has ParkingFloors → Composition
Car is-a Vehicle → Inheritance
Booking has-a reference to User → Association
2. Multiplicity?
One ParkingLot → Many ParkingFloors (1:N)
One User → Many Bookings (1:N)
One Show → Many Seats (1:N)
One Booking → One Seat (1:1)
3. Lifecycle dependency
If the parent dies, does the child die too? → Composition (strong). Can the child exist independently? → Aggregation (weak).
Step 4 — The "What Varies?" Question
This is how you identify where design patterns are actually needed — not by memorizing pattern names, but by asking what's likely to change.
Fig. 3
What varies maps directly to which pattern
Every pattern in Step 8 traces back to one of these questions — the pattern is the answer, "what varies" is the question you ask to find it.
02Draw Class Diagram 3–4 min
"Let me model the core entities and their relationships."
Class diagram notation — text format for use in an interview
Rendered example — same notation as an actual diagram
Fig. 4
Interface, abstract class, and concrete class relationships
Dashed hollow arrow = implements an interface. Solid hollow arrow = extends a class. Solid thin arrow = association ("has-a" reference).
Relationship notation — draw on whiteboard / write in text
Relationship
Notation
Meaning
Association
A ────────> B
A has a reference to B
Dependency
A - - - -> B
A uses B temporarily
Aggregation
A <>───────> B (hollow diamond)
A has B, B can exist alone
Composition
A <◆>──────> B (filled diamond)
A owns B, B cannot exist alone
Inheritance
A ────────|> B (hollow arrow)
A extends B
Implementation
A - - - -|> B (dashed hollow arrow)
A implements B
What to include in every class diagram
For every class, interface, or abstract class, write out: fields with type and visibility (+, -, #); methods with parameters and return types; stereotypes like <<interface>>, <<abstract>>, <<enum>>, <<singleton>>; and mark static members explicitly (underline or the word static).
03Draw Sequence Diagram 2–3 min, if asked or if flow is complex
Sequence diagram: KYC request across Client, Controller, Service, Repository, External
Solid arrows are calls; dashed arrows are returns. Left to right always follows Client → Controller → Service → Repository → External.
Rules for the sequence diagram
Left to right: Client → Controller → Service → Repository → External
Show return arrows (dashed or explicitly labeled)
Show alternate flows with [alt] / [opt] blocks
Show loops with [loop] blocks
[alt: item found]
Service --> Repository: findById()
Repository --> Service: Item
[else: item not found]
Service --> Client: throw ItemNotFoundException
[end]
[loop: for each item in cart]
Service --> PricingService: calculatePrice(item)
[end]
04Define Relationships 2 min
"Let me define the relationships before writing code."
Decision tree — what to use when
Fig. 6
Relationship and pattern decision tree
Two more cases beyond this diagram: pluggable algorithms → Strategy Pattern; object creation without specifying the exact class → Factory Pattern.
Senior-level phrases — relationships
"I use an interface here because multiple unrelated classes need this capability, allowing plug-and-play implementations."
"I use an abstract class because these entities share common state and behavior — the base handles the what, subclasses handle the how."
"I prefer composition over inheritance here to avoid tight coupling. If I inherit, I'm locked into a hierarchy. Composition gives me flexibility."
"This is a has-a relationship, not an is-a, so I compose rather than extend."
05Folder Structure 1 min — say this before writing code
"Let me lay out the package structure before writing any code."
Fig. 7
Standard LLD package structure
Say the structure out loud before writing a single class — it signals you're thinking in terms of separation of concerns, not improvising file-by-file.
06Write Core Code 5–8 min
Code comment style — mandatory
Every non-trivial class or method needs a comment block naming the pattern, the reason for it, and the principle it satisfies:
/*
* [Pattern/Principle]: Strategy Pattern
* [Why]: So we can swap pricing algorithms at runtime
* without modifying OrderService.
* [Principle]: OCP -- Open for extension, closed for modification.
*/
File template — every file must have this shape
package com.example.service;
/**
* What it does: Processes orders end to end.
* Why it exists: Central coordination point -- keeps Controller thin.
* Pattern: Follows SRP -- only order processing logic here.
*/
public class OrderService {
// Fields -- explain why each exists
private final PaymentStrategy paymentStrategy; // Strategy pattern -- pluggable payment
private final Map<String, Order> orderStore; // ConcurrentHashMap for thread safety
// Constructor injection -- DIP: depend on abstraction, not concrete class
public OrderService(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
this.orderStore = new ConcurrentHashMap<>();
}
public Order placeOrder(String userId, List<Item> items) {
// Step 1: Validate inputs
validateInput(userId, items);
// Step 2: Build order (Builder pattern if complex)
Order order = new Order(UUID.randomUUID().toString(), userId, items, OrderStatus.PENDING);
// Step 3: Process payment
paymentStrategy.pay(order.getTotalAmount());
// Step 4: Persist
orderStore.put(order.getId(), order);
return order;
}
}
Fig. 8
placeOrder's four internal steps
Every service method that does real work should read as this kind of numbered, commented sequence — it's what lets an interviewer follow your code without asking.
Model template
public class Order {
private final String id; // immutable
private final String userId;
private List<Item> items;
private OrderStatus status; // mutable -- lifecycle changes
private final LocalDateTime createdAt;
// Constructor, getters, equals(), hashCode(), toString()
}
/*
* Capability contract -- any class that can process payments implements this.
* Allows swapping Credit, UPI, Wallet without touching OrderService.
*/
public interface PaymentStrategy {
void pay(double amount);
String getPaymentMode();
}
Abstract class template
/*
* Abstract class -- all notification channels share senderId + timestamp logic.
* Subclasses only implement the channel-specific send().
*/
public abstract class Notification {
protected final String senderId;
protected final LocalDateTime timestamp;
protected Notification(String senderId) {
this.senderId = senderId;
this.timestamp = LocalDateTime.now();
}
// Common behavior
public void log() {
System.out.println("[" + timestamp + "] Notification from " + senderId);
}
// Subclasses must implement
public abstract void send(String message);
}
Custom exception hierarchy template
// Base exception -- callers can catch at right granularity
public class AppException extends RuntimeException {
private final String errorCode;
public AppException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
public class ItemNotFoundException extends AppException {
public ItemNotFoundException(String itemId) {
super("ITEM_NOT_FOUND", "Item not found: " + itemId);
}
}
public class InsufficientBalanceException extends AppException {
public InsufficientBalanceException(double required, double available) {
super("INSUFFICIENT_BALANCE",
"Required: " + required + ", Available: " + available);
}
}
public class InvalidStateException extends AppException {
public InvalidStateException(String currentState, String operation) {
super("INVALID_STATE",
"Cannot perform " + operation + " in state: " + currentState);
}
}
Fig. 9
Custom exception hierarchy
Every specific exception carries its own errorCode — callers can catch broadly at AppException or narrowly at the specific subtype, and logs always have a stable code to filter on.
07Concurrency critical for Adobe / Kotak-style interviews
"Since multiple requests can hit this simultaneously, let me ensure thread safety."
Concurrency toolkit — when to use what
Tool
When to use
ConcurrentHashMap
Thread-safe map, no full lock needed
synchronized block
Simple mutual exclusion on a small critical section
Limit concurrent access (e.g., max 10 concurrent bookings)
BlockingQueue
Producer-consumer pattern
Fig. 10
Concurrency tool decision tree
This is the "30-second rule" version — enough to answer instantly when an interviewer asks "why this tool and not that one."
Code patterns
// CAS with AtomicInteger
AtomicInteger seatCount = new AtomicInteger(100);
boolean booked = seatCount.compareAndSet(currentVal, currentVal - 1);
// Synchronized critical section
public synchronized boolean bookSeat(String seatId) {
if (!availableSeats.contains(seatId)) throw new SeatUnavailableException(seatId);
availableSeats.remove(seatId);
bookedSeats.put(seatId, userId);
return true;
}
// ReentrantLock for fine-grained control
private final ReentrantLock lock = new ReentrantLock();
public void transfer(Account from, Account to, double amount) {
lock.lock();
try {
from.debit(amount);
to.credit(amount);
} finally {
lock.unlock(); // always in finally
}
}
// Idempotency check
private final Set<String> processedRequestIds = ConcurrentHashMap.newKeySet();
public void process(String requestId, Order order) {
if (!processedRequestIds.add(requestId)) {
return; // duplicate -- already processed
}
// actual processing
}
Fig. 11
ReentrantLock transfer — why unlock belongs in finally
This is the single most common bug pattern interviewers probe for — always show unlock() inside finally, never just at the end of the method body.
Senior-level phrases — concurrency
"I use ConcurrentHashMap instead of HashMap because multiple threads will read and write simultaneously — ConcurrentHashMap gives us segment-level locking without blocking the entire map."
"I use AtomicInteger with CAS here to avoid a synchronized block on the counter — it's lock-free and more performant under high contention."
"I add an idempotency check using a ConcurrentHashMap key set — if the same requestId comes twice, we return early without double-processing."
08Design Patterns use when needed
Call out the pattern by name and explain why — naming it without justifying it reads as pattern-matching for its own sake, not engineering judgment.
Pattern
Use when
Senior phrase
Strategy
Multiple algorithms, interchangeable at runtime
"I use Strategy so I can swap logic without touching the service class."
Factory
Object creation logic is complex or varies
"I use Factory to centralize creation and hide the new keyword from callers."
Builder
Object has many optional fields
"Builder pattern prevents telescoping constructors and makes construction readable."
Singleton
Global config, registry, thread pool
"I use Singleton for the config manager — one instance, globally accessible."
Observer
Event notifications to multiple subscribers
"Observer decouples the event source from handlers — add/remove listeners without touching core logic."
State
Entity has lifecycle (PENDING → CONFIRMED → etc.)
"State pattern externalizes lifecycle transitions — each state knows what's allowed."
Decorator
Add behavior without modifying class
"Decorator wraps the base service and adds cross-cutting concerns like logging or rate-limiting."
Template Method
Algorithm skeleton, steps vary
"Template Method defines the flow in the abstract class — subclasses plug in the specifics."
State Pattern template — common in LLD interviews
public interface OrderState {
void confirm(OrderContext ctx);
void ship(OrderContext ctx);
void cancel(OrderContext ctx);
}
public class PendingState implements OrderState {
public void confirm(OrderContext ctx) {
ctx.setState(new ConfirmedState());
}
public void ship(OrderContext ctx) {
throw new InvalidStateException("PENDING", "ship");
}
public void cancel(OrderContext ctx) {
ctx.setState(new CancelledState());
}
}
public class OrderContext {
private OrderState currentState = new PendingState();
public void setState(OrderState state) { this.currentState = state; }
public void confirm() { currentState.confirm(this); }
public void ship() { currentState.ship(this); }
public void cancel() { currentState.cancel(this); }
}
Fig. 12
Order lifecycle as the State pattern
Each state class only implements the transitions valid from that state — calling ship() on a PendingState throws InvalidStateException rather than needing an if-chain somewhere else in the codebase.
09Design Principles (SOLID) always mention at least 2
"Let me call out the SOLID principles I'm applying."
Principle
What to say
SRP
"Each class has one reason to change — OrderService only handles order logic, PaymentService only handles payment."
OCP
"I can add a new payment type by implementing PaymentStrategy — no existing code changes."
LSP
"Any subclass of Notification can replace the base without breaking behavior."
ISP
"I split the interface into Payable and Refundable — callers depend only on what they use."
DIP
"OrderService depends on PaymentStrategy interface, not CreditCardPayment — high-level modules don't depend on low-level."
Composition over Inheritance
"I compose OrderService with a PaymentStrategy rather than extending a base payment class."
Encapsulation
"All fields are private — state changes only through validated methods."
Fail Fast
"I validate at entry points so invalid state never propagates deep into the system."
10Edge Case Checklist before finishing
"Let me quickly validate the failure scenarios."
Go through this list out loud:
☐
Null inputs — null userId, null item list?
☐
Empty collections — empty cart, zero items?
☐
Negative / zero values — negative price, zero quantity?
☐
Duplicate requests — same order placed twice?
☐
Concurrent access — two threads booking same seat?
☐
Invalid state transition — shipping a cancelled order?
☐
Max limits exceeded — cart limit, rate limit, booking cap?
☐
External service failure — payment gateway down?
☐
Partial failure — one item fails, what happens to rest?
☐
Resource exhaustion — out of stock, no available rooms?
11Logging add after core code
"I'll add structured logging — no sensitive data."
Card numbers, passwords, tokens, and full PII never belong in a log line, structured or not — logs get shipped to aggregators, retained for months, and read by more people than the code itself. Log the requestId and enough context to trace the failure, never the payload that made the failure sensitive.
12Scaling Discussion if asked
"If this needs to scale beyond a single JVM..."
Fig. 13
Scaling beyond a single JVM
Notice the Service Layer only has three downstream dependencies drawn — cache, database, queue — because everything above it (gateway, load balancer) exists to get a request there safely, and everything below exists to let that layer stay stateless.
Quick-reference cheat sheet
Opening lines
Moment
What to say
Start
"Before implementation, let me clarify a few requirements."
Entities
"Let me identify core entities and model them."
Relationships
"Let me define relationships before writing any code."
Concurrency
"Since multiple requests can hit this simultaneously, I'll ensure thread safety using..."
Review
"Let me quickly validate the edge cases."
Refactor
"I'll start with a simple design and refactor using patterns if needed."
Scale
"If this needs to go beyond single JVM, I'd add caching and a queue layer."
Relationship decision — the 30-second rule
Shared contract, unrelated classes
Interface
Shared state + behavior, related
Abstract Class
Has-a, independent lifecycle
Aggregation (field reference)
Has-a, dependent lifecycle
Composition (create inside)
Pluggable algorithm
Strategy pattern
Concurrency decision — the 30-second rule
Thread-safe map
ConcurrentHashMap
Simple counter
AtomicInteger
Lock with tryLock / fairness
ReentrantLock
Read-heavy
ReadWriteLock
Limit parallel access
Semaphore
Producer-consumer
BlockingQueue
Prevent duplicate processing
Idempotency key in ConcurrentHashMap
End of Rulebook
Run every step. Call out every step name. The structure is the signal — an interviewer watching you visibly move through requirements → entities → diagrams → relationships → folder structure → code → concurrency → patterns → SOLID → edge cases → logging → scaling is watching someone who has done this before, independent of whether the specific problem is one you've seen.