Lesson 30 of 31
LLD Interview Checklist
This is a reference to keep next to you while practicing and to run in your head during the interview. It compresses the course into steps, signals and checks. Each item links to the article that explains it. The full method is in the LLD interview framework.
The 45-minute run sheet
| Minutes | Step | Output |
|---|---|---|
| 0-5 | Clarify requirements | Written list: must have, out of scope, numbers |
| 5-10 | Identify entities and relationships | Nouns with owner and multiplicity |
| 10-15 | Sketch API and class diagram | Public methods and error cases |
| 15-20 | Decide what varies, apply patterns | One line per pattern: "because requirement X varies" |
| 20-38 | Implement core paths | Compiling types, main flow, one error path |
| 38-43 | Walk a scenario, check edge cases | Traced example, invariants stated |
| 43-45 | Extensions and trade-offs | Concurrency, persistence, scale |
Stop designing when you can trace one full request through your types. Extra layers that no requirement asked for cost more than they earn.
Requirements checklist
Ask these before drawing anything. Write the answers down.
- Actors. Who calls the system, and are there different roles?
- Core operations. What are the three to five verbs? (park, unpark, book, cancel.)
- Rules. What are the pricing, limits, expiry or eligibility rules, and which of them might change?
- Failure. What should happen on invalid input, on a full system, on a duplicate?
- Concurrency. One thread or many? If many, what is shared? See thread-safe components.
- Time. Does behavior depend on the clock? If so, plan to inject it.
- Money and units. Integer cents (
long), neverdouble. - Scale and persistence. In memory only? How many items? (Usually in memory, thousands.)
- Out of scope. Say aloud what you will not build (UI, network, database).
Entity and relationship checklist
- List nouns, then filter: is it data with identity (entity), a value (record), a rule (strategy), or a fixed set of cases (enum or sealed interface)?
- For each relationship, name the owner and the multiplicity. Draw it as in class diagrams and relationships.
- Prefer composition and IDs to object references between entities. See object graphs and references.
- One aggregate root per consistency boundary: the type that enforces the invariants of its parts.
- Every invariant has an owner. If two types both "should" check it, move it into one.
Pattern-signal table
Use a pattern only when a requirement makes something vary. The sentence in the first column is what you hear; the second column is the tool. Pattern details are in the lessons linked in the table and in SOLID in Java.
| Signal in the requirements | Tool | Java shape |
|---|---|---|
| "Different pricing / fee / discount rules" | Strategy | interface PricingPolicy or a lambda |
| "Fixed set of types: coin, note, card" | Enum | enum with fields, or a sealed interface of records with an exhaustive switch |
| "Third parties add new kinds" | Open interface | a plain interface with implementations supplied by callers |
| "Behavior depends on the current state" | State machine | enum of states, or a sealed interface of state records, see state machines with sealed types |
| "Notify others when X happens" | Observer | listeners, queues, see event bus |
| "Undo / redo / audit log" | Command | Command interface or sealed records with apply and revert |
| "Build a complex object with options" | Builder | builder with a validating build(), or a record with a compact constructor |
| "One of several creation paths" | Factory | static factory method or a method on the enum |
| "Add logging or caching around a service" | Decorator | wrapper class implementing the same interface |
| "Tree: folders contain files and folders" | Composite | sealed interface with a leaf record and a folder record holding a List of children |
| "Process a request through stages" | Chain / pipeline | List<Handler> run in order |
| "Many readers, evict oldest / least used" | Cache with eviction | map plus order structure, see LRU cache |
| "Multiple threads use it" | Internal synchronization | private lock or a ConcurrentHashMap behind a small public API |
| "Work handed to background workers" | Producer-consumer | blocking queue |
| "Only one instance should exist" | Usually none | pass it in; avoid global state |
| "Must be testable without real time" | Dependency injection | Clock or a small Ticker interface, see testability |
If no row matches, do not force a pattern. A plain class with methods is a valid answer.
Java idiom checklist
Scan your code against these before saying "done". See modeling domains in Java, encapsulation and invariants and errors in domain design.
- Closed set of cases is an
enumor asealed interface, not an open interface with three implementations. Open set is a plain interface. - IDs and amounts are records (
record UserId(int value)), so aSpotIdcannot be passed as aUserId. - Money is integer cents in a record, with
Math.addExactwhere overflow matters. - Fields are
private final, and constructors (or compact record constructors) throwIllegalArgumentExceptionwhen input is invalid. - Errors are domain exceptions (or a sealed result type), one type per rule that can fail, carrying the data a caller needs. No bare
RuntimeException("...")strings. - No reference cycles between entities. Use IDs and a central owner. Use a parent reference only inside one tree.
- Exposed collections are copies or unmodifiable views (
List.copyOf,Collections.unmodifiableList). Mutation goes through methods that keep the invariant. -
Optionalas a return type for "may be absent", never a field or parameter, and no-1or empty string meaning "none". - Invalid states unrepresentable: a sealed variant carries only the data valid in that state.
- Deterministic collections:
TreeMaporLinkedHashMap(or sorted output) when order is printed. - Time and randomness are injected (
Clock, a seededRandom), not read from the system. - Public API is small.
privateby default,publiconly what callers need. - One
mainor a fewassertchecks that demonstrate the happy path and one failure.
Small examples worth remembering
Three tiny fragments that cover the idioms interviewers look for most.
Record IDs, validating constructor, domain exception
record AccountId(int value) {}
record Cents(long value) {
Cents {
if (value < 0) {
throw new IllegalArgumentException("cents must not be negative: " + value);
}
}
Cents plus(Cents other) {
return new Cents(Math.addExact(value, other.value));
}
Cents minus(Cents other) {
return new Cents(value - other.value);
}
}
sealed class WalletException extends RuntimeException permits ZeroAmountException, InsufficientFundsException {
WalletException(String message) {
super(message);
}
}
final class ZeroAmountException extends WalletException {
ZeroAmountException() {
super("amount must be positive");
}
}
final class InsufficientFundsException extends WalletException {
InsufficientFundsException(Cents needed, Cents available) {
super("need %d cents, have %d".formatted(needed.value(), available.value()));
}
}
final class Wallet {
private final AccountId owner; // private: only methods can change state
private Cents balance = new Cents(0);
Wallet(AccountId owner) {
this.owner = owner;
}
AccountId owner() {
return owner;
}
Cents balance() {
return balance;
}
void deposit(Cents amount) {
requirePositive(amount);
balance = balance.plus(amount);
}
void withdraw(Cents amount) {
requirePositive(amount);
if (balance.value() < amount.value()) {
throw new InsufficientFundsException(amount, balance);
}
balance = balance.minus(amount);
}
private static void requirePositive(Cents amount) {
if (amount.value() == 0) {
throw new ZeroAmountException();
}
}
}
public class Main {
public static void main(String[] args) {
var wallet = new Wallet(new AccountId(7));
wallet.deposit(new Cents(500));
try {
wallet.withdraw(new Cents(0));
} catch (WalletException e) {
System.out.println(e.getClass().getSimpleName());
}
try {
wallet.withdraw(new Cents(900));
} catch (WalletException e) {
System.out.println("error: " + e.getMessage());
}
wallet.withdraw(new Cents(200));
System.out.println("owner " + wallet.owner() + " balance " + wallet.balance().value());
}
}ZeroAmountException
error: need 900 cents, have 500
owner AccountId[value=7] balance 300The compact constructor makes a negative Cents impossible. The sealed exception type lists every domain failure, and callers catch WalletException once.
Enum for the closed set, interface for the open behavior
import java.util.Map;
import java.util.TreeMap;
// Closed set: the vehicle sizes are fixed by the requirements.
enum Size {
SMALL(200),
LARGE(400);
final int centsPerHour;
Size(int centsPerHour) {
this.centsPerHour = centsPerHour;
}
}
// Open behavior: pricing rules change, so callers may supply their own.
@FunctionalInterface
interface Pricing {
long centsFor(Size size, int hours);
}
public class Main {
public static void main(String[] args) {
Pricing flat = (size, hours) -> (long) size.centsPerHour * hours;
// first hour full price, later hours half price
Pricing tiered = (size, hours) -> size.centsPerHour + (long) size.centsPerHour / 2 * Math.max(hours - 1, 0);
Map<String, Pricing> policies = new TreeMap<>(Map.of("flat", flat, "tiered", tiered));
policies.forEach((name, policy) -> System.out.println(
"%s: small 3h = %d, large 3h = %d".formatted(
name, policy.centsFor(Size.SMALL, 3), policy.centsFor(Size.LARGE, 3))));
}
}flat: small 3h = 600, large 3h = 1200
tiered: small 3h = 400, large 3h = 800The enum is exhaustive: adding a size means adding a constant, and every switch over Size without a default stops compiling until it handles the new case.
Inject the clock
interface Ticker {
long nowSeconds();
}
final class FakeTicker implements Ticker {
private long now;
FakeTicker(long start) {
now = start;
}
void set(long seconds) {
now = seconds;
}
@Override
public long nowSeconds() {
return now;
}
}
final class Session {
private final Ticker ticker;
private final long startedAt;
private final long ttlSeconds;
Session(Ticker ticker, long ttlSeconds) {
this.ticker = ticker;
this.startedAt = ticker.nowSeconds();
this.ttlSeconds = ttlSeconds;
}
boolean isExpired() {
return ticker.nowSeconds() >= startedAt + ttlSeconds;
}
}
public class Main {
public static void main(String[] args) {
var ticker = new FakeTicker(1_000);
var session = new Session(ticker, 60);
System.out.println("expired at t+0: " + session.isExpired());
ticker.set(1_059);
System.out.println("expired at t+59: " + session.isExpired());
ticker.set(1_060);
System.out.println("expired at t+60: " + session.isExpired());
}
}expired at t+0: false
expired at t+59: false
expired at t+60: trueThe session reads time only through Ticker. Production code passes an implementation backed by java.time.Clock; the test passes the fake.
Concurrency checklist
If the interviewer says "make it thread-safe" or "handle concurrent requests":
- Name what is shared and the invariant that spans several fields.
- One lock per invariant, critical section as small as possible.
- Public methods are atomic operations; no locks in the public API.
- Never call callbacks or block on a queue while holding a lock.
- Several locks means a written global order (for example ascending ID).
- Read-heavy: consider
ReadWriteLock. A single counter or flag:AtomicInteger,AtomicBooleanorLongAdder. - Handoff between threads: a queue with a defined close behavior.
- Say what is not guaranteed (for example a non-atomic snapshot across shards).
Details: thread-safe components, event bus and observers, blocking queue.
Testing checklist
- One test per business rule, named after the rule.
- One test per error variant.
- Property to assert after concurrent runs: totals and invariants, not interleavings.
- Fake clock and fake dependencies through interfaces or lambdas.
- Print or assert only deterministic data (sorted, or from a
TreeMap).
Mistakes that cost the most
| Mistake | Fix |
|---|---|
| Starting to code before clarifying | Spend the first five minutes on requirements |
| A pattern for every noun | A pattern only where a requirement varies |
| God object holding all logic | One responsibility per type, see SOLID in Java |
| Public fields everywhere | Private fields and methods that keep invariants |
double for money |
Integer cents in a record |
| Entities that hold references to each other | IDs plus a central owner |
| Stringly-typed errors and states | Exception types, state enum |
| Ignoring the clock | Clock interface, fake in tests |
| "It is thread-safe" without saying how | Name the lock, the invariant and the order |
| Silence while thinking | Narrate the trade-off you are weighing |
Follow-up questions to prepare for
- "How would you add a new rule or a new type?" Point at the sealed interface or enum
switchyou would extend (the compiler flags every non-exhaustiveswitch) or the interface you would implement. - "What if this had to be distributed or persistent?" Introduce a storage interface, keep the domain unchanged.
- "How would you test this?" Fake clock, in-memory repository, deterministic output.
- "What is the time complexity?" State it for the hot operation and the data structure behind it.
- "What breaks under concurrency?" The check-then-act sequences; explain the atomic method that replaces them.
Summary
- Run the sheet in order: requirements, entities, API, variation and patterns, implementation, scenario walk-through, extensions.
- Use the signal table: a pattern is justified only by a requirement that varies.
- Model closed sets as enums or sealed interfaces, open behavior as interfaces, IDs and money as records, failures as domain exceptions.
- Keep fields private, make states unrepresentable, prefer IDs over object references between entities.
- Inject time and dependencies, and keep output deterministic so tests are trivial.
- For concurrency, name the shared state, the invariant, the lock scope and the lock order.
- Practice with the plan in the LLD study plan.
