Interview Prep / Low-Level Design & Java

The LLD + Java Interview Rulebook

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.

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

Functional vs non-functional requirements feed different design decisionsFunctional requirements, gathered first, describe what the system does and feed entities, methods and relationships. Non-functional requirements, gathered second, describe how the system behaves and feed data structures, patterns and concurrency.One design · two inputsfeedsfeedsthenFunctional RequirementsWhat the system DOES1entitiesmethodsrelationshipsNon-Functional RequirementsHow the system BEHAVES2data structurespatternsconcurrency
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."
AreaQuestions to ask
ScaleHow many users? Expected RPS? Read-heavy or write-heavy?
ConcurrencyCan multiple users/threads access the same resource simultaneously?
ConsistencyStrong or eventual consistency? (e.g., seat booking must be strongly consistent)
LatencyAny SLA? Response time target? (e.g., < 200ms)
Availability99.9% uptime? Can the system go down briefly?
DurabilityIn-memory OK or must data survive restart?
IdempotencyShould the same request twice produce the same result?
ExtensibilityWill new types / strategies be added later? (drives pattern choice)
Failure HandlingWhat happens on partial failure? Retry? Rollback? Compensate?
ScopeSingle 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:

QuestionIf YES →If NO →
Does it have its own identity (unique ID)?Strong Entity candidateMight be a value object / field
Does it have state that changes over time?Definite EntityMight be just an enum / constant
Does it have behavior (does things or things happen to it)?Needs its own classMight 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 four entity archetypesFour archetypes: the Actor who uses the system, the Resource being managed, the Transaction created when an Actor interacts with a Resource, and the Coordinator or Manager that orchestrates the system (often a Singleton). The Coordinator references a collection of Resources and creates Transactions when an Actor acts; each Transaction holds an actorRef and a resourceRef.actscreatesreferences acollection ofactorRefresourceRefArchetype 4The COORDINATOR / MANAGERWhat orchestrates the system? (Often a Singleton)ExamplesParkingLotLibraryVendingMachineElevatorControllerAlways hascollection of Resources, business rules, state mgmt4Archetype 1The ACTORWho uses / interacts with thesystem?ExamplesUserCustomerDriverAdminAgentAlways hasid, name, contact info,role/status1Archetype 3The TRANSACTIONWhat happens when Actorinteracts with Resource?ExamplesBookingPaymentTicketRideOrderAlways hasid, timestamp, actorRef,resourceRef, status3Archetype 2The RESOURCEWhat is being managed / allocated/ consumed?ExamplesParkingSpotSeatRoomBookTicketAlways hasid, availability status,type/category2
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

What varies maps directly to which patternStarting from the question "What might change or vary?": fee calculation varies maps to Strategy; object creation varies to Factory or Abstract Factory; notification mechanism varies to Observer; object state changes significantly to State; only one instance should exist to Singleton; adding behaviour dynamically to Decorator; decoupling complex subsystems to Facade.What mightchange or vary?FEE CALCULATION varies?Strategy PatternOBJECT CREATION varies?Factory / Abstract FactoryNOTIFICATION mechanism varies?Observer PatternOBJECT STATE changes significantly?State PatternOnly ONE instance should exist?SingletonADD behavior dynamically?DecoratorDECOUPLE complex subsystems?Facade
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

+----------------------------+
|       ClassName            |  <- Concrete Class
+----------------------------+
| - privateField: Type       |
| # protectedField: Type     |
| + publicField: Type        |
+----------------------------+
| + publicMethod(): ReturnType|
| - privateMethod(): void    |
| + staticMethod(): Type     |
+----------------------------+

<<interface>>
+----------------------------+
|       InterfaceName        |
+----------------------------+
| + method1(): void          |
| + method2(): String        |
+----------------------------+

<<abstract>>
+----------------------------+
|    AbstractClassName       |
+----------------------------+
| # sharedField: Type        |
+----------------------------+
| + concreteMethod(): void   |
| + abstractMethod(): void   |  <- italicize or mark (abstract)
+----------------------------+

Rendered example — same notation as an actual diagram

Fig. 4

Interface, abstract class, and concrete class relationships

Interface, abstract class, and concrete class relationshipsPaymentStrategy is an interface with pay(amount) and getMode(); CreditCardPayment implements it (dashed hollow arrow). Notification is an abstract class with senderId, log() and abstract send(msg); EmailNotification extends it (solid hollow arrow). OrderService holds a paymentStrategy field of type PaymentStrategy, a has-a association (solid thin arrow).Implements an interfaceExtends a classimplementsextendshas-a (association)<<interface>>PaymentStrategy+ pay(amount): void+ getMode(): StringCreditCardPayment- cardNumber: String+ pay(amount): void+ getMode(): String<<abstract>>Notification# senderId: String+ log(): void+ send(msg): void (abstract)EmailNotification+ send(msg): void+ getInbox(): StringOrderService- paymentStrategy: PaymentStrategyimplements an interfaceextends a classassociation ("has-a" reference)
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

RelationshipNotationMeaning
AssociationA ────────> BA has a reference to B
DependencyA - - - -> BA uses B temporarily
AggregationA <>───────> B (hollow diamond)A has B, B can exist alone
CompositionA <◆>──────> B (filled diamond)A owns B, B cannot exist alone
InheritanceA ────────|> B (hollow arrow)A extends B
ImplementationA - - - -|> 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

"Let me trace the flow for the primary use case."

Sequence diagram notation — text format

Client          Controller                Service                Repository            External
  |                 |                        |                        |                    |
  |-- POST /api/v1/kyc ------------------->|                        |                    |
  |   (Create KYC Request)                |                        |                    |
  |                 |-- validate() ------>|                        |                    |
  |                 |                     |-- GET /kyc/{id} ------>|                    |
  |                 |                     |                        |-- query() -------->|
  |                 |                     |                        |<-- result ---------|
  |                 |                     |<-- entity -------------|                    |
  |                 |                     |-- GET /external/user/{id} ----------------->|
  |                 |                     |<---------------------- external data -------|
  |                 |                     |-- process()            |                    |
  |                 |                     |-- PUT /kyc/{id} ------>|                    |
  |                 |                     |                        |-- save() ----------|
  |                 |<-- response --------|                        |                    |
  |<-- 201 Created --|                    |                        |                    |

Rendered example — KYC request flow

Fig. 5

Sequence diagram: KYC request across Client, Controller, Service, Repository, External

Sequence diagram: KYC request across Client, Controller, Service, Repository, ExternalClient sends POST /api/v1/kyc to Controller, which calls validate() on Service. Service calls GET /kyc/{id} on Repository, which runs query() against External; result returns to Repository and the entity to Service. Service calls GET /external/user/{id} on External and receives external data, runs process(), then PUT /kyc/{id} to Repository, which calls save() on External. Service returns a response to Controller, which returns 201 Created to Client.1POST /api/v1/kyc2validate()3GET /kyc/{id}4query()5result6entity7GET /external/user/{id}8external data910PUT /kyc/{id}11save()12response13201 CreatedClientControllerServiceRepositoryExternalprocess()solid arrow = calldashed arrow = returnClient → 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

Relationship and pattern decision treeNeed a shared contract across unrelated classes: use an interface (e.g. Payable, Notifiable, Searchable). Need shared state and behaviour with variation: an abstract class (e.g. Vehicle, Employee, Shape). Need to reuse behaviour without an IS-A relationship: composition, e.g. OrderService has-a PaymentGateway. Need IS-A with full override: inheritance, e.g. CreditCard extends Card.yesyesyesyesNeed shared CONTRACT acrossunrelated classes?INTERFACEe.g. Payable, Notifiable, SearchableNeed shared STATE + BEHAVIORwith variation?ABSTRACT CLASSe.g. Vehicle, Employee, ShapeNeed to REUSE behavior withoutIS-A relationship?COMPOSITION (has-a)e.g. OrderService has-a PaymentGatewayNeed IS-A with full override?INHERITANCE (extends)e.g. CreditCard extends Card
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

Standard LLD package structureUnder project-name/src/main/java/com/example/: model, enums, interfaces, service, strategy, factory, observer, state, repository, controller and exception packages, plus Application.java with main(), demo and test scenarios. Each package is listed with its responsibility.project-name/src/main/java/com/example/model/Plain entities — fields, getters/setters, equals, hashCodeenums/Status, Type, Category enumsinterfaces/Capability contracts (Payable, Notifiable, ...)service/Core business logicstrategy/Strategy pattern implementationsfactory/Factory / Abstract Factory classesobserver/Observer pattern — EventListener, EventPublisherstate/State pattern — if lifecycle management is neededrepository/In-memory storage — HashMap / ConcurrentHashMapcontroller/Entry point — delegates to serviceexception/Custom exception hierarchyApplication.javamain(), demo, test scenarios
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

placeOrder's four internal stepsOrderService.placeOrder runs four numbered steps in order: 1 validate the input and fail fast, 2 build the order (Builder if complex), 3 process payment via the Strategy, 4 persist it in a ConcurrentHashMap, then return the order.Inside OrderService.placeOrder()CallerplaceOrder(userId, items)Order returnedreturn order;Validatefail fast1Build orderBuilder if complex2Process paymentvia Strategy3PersistConcurrentHashMap4validateInput(…)new Order(…, PENDING)paymentStrategy.pay(…)orderStore.put(…)
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()
}

Enum template

public enum OrderStatus {
    PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}

Interface template

/*
 * 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

Custom exception hierarchyAppException extends RuntimeException and carries an errorCode. ItemNotFoundException, InvalidStateException and InsufficientBalanceException each extend AppException with their own stable code, so callers can catch broadly at AppException or narrowly at a subtype.Catch narrowlyextendsRuntimeExceptionAppExceptionerrorCode + messageCatch broadlycatch (AppException e)Logs filter on itstable errorCodeItemNotFoundExceptionerrorCode = ITEM_NOT_FOUNDInvalidStateExceptionerrorCode = INVALID_STATEInsufficientBalanceExceptionerrorCode = INSUFFICIENT_BALANCE
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

ToolWhen to use
ConcurrentHashMapThread-safe map, no full lock needed
synchronized blockSimple mutual exclusion on a small critical section
ReentrantLockNeed tryLock, fairness, or lock across methods
AtomicInteger / AtomicLongCounter increments without synchronized
AtomicReference<T>CAS on a single object reference
volatileSingle variable visibility across threads
ReadWriteLockRead-heavy workload — multiple readers, exclusive writer
SemaphoreLimit concurrent access (e.g., max 10 concurrent bookings)
BlockingQueueProducer-consumer pattern
Fig. 10

Concurrency tool decision tree

Concurrency tool decision treeA top-down decision tree. Thread-safe map: ConcurrentHashMap. Simple counter: AtomicInteger. Lock with tryLock or fairness: ReentrantLock. Read-heavy workload: ReadWriteLock. Limit parallel access: Semaphore. Producer-consumer: BlockingQueue. Prevent duplicate processing: an idempotency key in a ConcurrentHashMap.yesnoyesnoyesnoyesnoyesnoyesnoyesThread-safe map?ConcurrentHashMapno full lock neededSimple counter?AtomicIntegerincrements without synchronizedLock with tryLock /fairness?ReentrantLocktryLock, fairness, lock across methodsRead-heavy workload?ReadWriteLockmultiple readers, exclusive writerLimit parallel access?Semaphoree.g. max 10 concurrent bookingsProducer-consumer?BlockingQueueproducer-consumer patternPrevent duplicateprocessing?Idempotency keyin ConcurrentHashMapWhich tool?
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

ReentrantLock transfer — why unlock belongs in finallyWith finally: lock.lock(), then try debit and credit; whether the block completes or an exception is thrown mid-transfer, finally lock.unlock() runs and the lock is always released. Without finally: the exception skips the unlock at the end of the method body, the lock is held forever and every other thread deadlocks waiting on it.With finallyWithout finallycompletesexception thrownmid-transferthrowslock.lock()1try { debit(); credit(); }from.debit(amount) · to.credit(amount)2finally { lock.unlock(); }runs regardless — lock always released3lock.lock()1debit(); credit();↑ if this throws…2lock.unlock();never reachedLock held foreverevery other thread deadlocks waiting on it
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.

PatternUse whenSenior phrase
StrategyMultiple algorithms, interchangeable at runtime"I use Strategy so I can swap logic without touching the service class."
FactoryObject creation logic is complex or varies"I use Factory to centralize creation and hide the new keyword from callers."
BuilderObject has many optional fields"Builder pattern prevents telescoping constructors and makes construction readable."
SingletonGlobal config, registry, thread pool"I use Singleton for the config manager — one instance, globally accessible."
ObserverEvent notifications to multiple subscribers"Observer decouples the event source from handlers — add/remove listeners without touching core logic."
StateEntity has lifecycle (PENDING → CONFIRMED → etc.)"State pattern externalizes lifecycle transitions — each state knows what's allowed."
DecoratorAdd behavior without modifying class"Decorator wraps the base service and adds cross-cutting concerns like logging or rate-limiting."
Template MethodAlgorithm 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

Order lifecycle as the State patternOrderContext starts in PENDING. confirm() moves PENDING to CONFIRMED, ship() moves CONFIRMED to SHIPPED, and cancel() moves PENDING to CANCELLED. Calling ship() on a PendingState throws InvalidStateException.confirm()ship()cancel()ship() ✕PENDINGPendingStateCONFIRMEDConfirmedStateSHIPPEDCANCELLEDCancelledStateInvalidStateException("PENDING", "ship")startThrows, no if-chainCalling ship() on a PendingStatethrows InvalidStateException —each state class implementsonly its valid transitions.
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."
PrincipleWhat 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."
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public Order placeOrder(String userId, List<Item> items) {
        String requestId = UUID.randomUUID().toString();
        log.info("PlaceOrder started | requestId={} userId={} itemCount={}",
                 requestId, userId, items.size());

        try {
            // ... logic ...
            log.info("PlaceOrder success | requestId={} orderId={}", requestId, order.getId());
            return order;
        } catch (AppException e) {
            log.error("PlaceOrder failed | requestId={} errorCode={} reason={}",
                      requestId, e.getErrorCode(), e.getMessage());
            throw e;
        }
        // NO: log.info("Card number: " + card.getNumber());  <- never log sensitive data
    }
}
Never log 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

Scaling beyond a single JVMA client request passes through the API Gateway (auth, rate limiting, SSL termination) and the Load Balancer (round-robin or least connections) to a stateless, horizontally scalable Service Layer. The service layer has three downstream dependencies: a Redis cache layer, a sharded and replicated database, and a Kafka message queue consumed by workers.Gets the request there safelyLets the service layer stay statelessAsyncClient RequestAPI GatewayAuth, Rate Limiting, SSL termination1Load BalancerRound-robin / Least connections2Service LayerStateless, horizontally scalable3Cache LayerRedis — hot data,sessions,idempotency keysDatabaseSharded/replicated —write primary,read replicaMessage QueueKafka — asyncnotifications,analytics, auditWorkersConsumer groups,independentscaling
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

MomentWhat 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 classesInterface
Shared state + behavior, relatedAbstract Class
Has-a, independent lifecycleAggregation (field reference)
Has-a, dependent lifecycleComposition (create inside)
Pluggable algorithmStrategy pattern

Concurrency decision — the 30-second rule

Thread-safe mapConcurrentHashMap
Simple counterAtomicInteger
Lock with tryLock / fairnessReentrantLock
Read-heavyReadWriteLock
Limit parallel accessSemaphore
Producer-consumerBlockingQueue
Prevent duplicate processingIdempotency 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.