Learn low-level design for the interview, not the textbook
Right classes & ownership
Methods that match the rules
Absorb follow-ups cleanly
Simple when simple works
What this Lattice guide is (and isn’t)
- Delivery framework — how to spend ~35 minutes without drowning.
- Fundamentals refresh — design principles, OOP concepts, the patterns that actually show up.
- Problem breakdowns — parking, rate limiter, elevator, booking, locker, filesystem, logging, Connect Four.
- Guided practice mindset — consume less, apply more; walk through solutions out loud.
Low-level design vs system design
Trip, a TripState enum, a PricingCalculator interface, and how those objects collaborate. No fleet-scale story required.from enum import Enum, auto
from typing import Optional, Protocol
class TripState(Enum):
REQUESTED = auto()
DRIVER_ASSIGNED = auto()
IN_PROGRESS = auto()
COMPLETED = auto()
CANCELLED = auto()
class PricingCalculator(Protocol):
def calculate_fare(self, trip: "Trip") -> float: ...
class Trip:
def __init__(self, trip_id: str, rider, pickup, dropoff):
self.id = trip_id
self.rider = rider
self.driver = None
self.pickup = pickup
self.dropoff = dropoff
self.state = TripState.REQUESTED
self.fare: Optional[float] = None
def assign_driver(self, driver) -> None:
if self.state is not TripState.REQUESTED:
raise ValueError(f"cannot assign in {self.state}")
self.driver = driver
self.state = TripState.DRIVER_ASSIGNED
def start(self) -> None:
if self.state is not TripState.DRIVER_ASSIGNED:
raise ValueError(f"cannot start in {self.state}")
self.state = TripState.IN_PROGRESS
def complete(self, calculator: PricingCalculator) -> None:
if self.state is not TripState.IN_PROGRESS:
raise ValueError(f"cannot complete in {self.state}")
self.fare = calculator.calculate_fare(self)
self.state = TripState.COMPLETED
def cancel(self) -> None:
if self.state is TripState.COMPLETED:
raise ValueError("cannot cancel a completed trip")
self.state = TripState.CANCELLED
enum TripState {
REQUESTED,
DRIVER_ASSIGNED,
IN_PROGRESS,
COMPLETED,
CANCELLED
}
interface PricingCalculator {
double calculateFare(Trip trip);
}
class Trip {
final String id;
final Rider rider;
Driver driver;
final Location pickup;
final Location dropoff;
TripState state = TripState.REQUESTED;
Double fare;
Trip(String tripId, Rider rider, Location pickup, Location dropoff) {
this.id = tripId;
this.rider = rider;
this.pickup = pickup;
this.dropoff = dropoff;
}
void assignDriver(Driver driver) {
if (state != TripState.REQUESTED) {
throw new IllegalArgumentException("cannot assign in " + state);
}
this.driver = driver;
this.state = TripState.DRIVER_ASSIGNED;
}
void start() {
if (state != TripState.DRIVER_ASSIGNED) {
throw new IllegalArgumentException("cannot start in " + state);
}
this.state = TripState.IN_PROGRESS;
}
void complete(PricingCalculator calculator) {
if (state != TripState.IN_PROGRESS) {
throw new IllegalArgumentException("cannot complete in " + state);
}
this.fare = calculator.calculateFare(this);
this.state = TripState.COMPLETED;
}
void cancel() {
if (state == TripState.COMPLETED) {
throw new IllegalArgumentException("cannot cancel a completed trip");
}
this.state = TripState.CANCELLED;
}
}
How LLD interviews vary by company and region
- Pseudocode vs real code. US big tech often wants class definitions and method logic in a real language (Java, Python, C++) even if nothing compiles. Many interviews in India and elsewhere lean toward structured pseudocode. Lattice breakdowns use readable Python-style pseudocode; classics include fuller references where useful.
- Pattern vocabulary. At big tech, naming Factory or Strategy matters less than sound reasoning — bring a pattern up when it naturally fits. Mid-size and some APAC loops ask about patterns more directly. Recognize the fit; don’t force a catalog.
- Ambiguity. Requirements are often vaguer outside the US — you’re expected to ask grounding questions and drive scope. US prompts can be slightly more defined, but clarifying questions still win points.
What interviewers score
- Problem analysis. Extract entities and responsibilities, ask a few clarifying questions, frame the problem before typing. Jumping straight into code is a common fail.
- Class design. Right responsibilities, clear method signatures, clean ownership and boundaries. Weak class design makes everything downstream harder.
- Code quality. Encapsulation, sensible state, composition vs inheritance judgment, naming, dependency direction — even in pseudocode, it should look disciplined.
- Extensibility. Most loops add a small follow-up. Strong candidates absorb it without a rewrite. Practical flexibility beats anticipating every possible future.
- Communication. Clear narrative, thoughtful tradeoffs, adjusting when probed. Talking out loud while you design is half the signal.
What changes at staff+
- Name seams early when variation is real (Strategy for pricing, Factory for types).
- Walk a failure path, not only the happy path.
- If asked “two threads enter,” show the race and the lock/atomic.
How to use this series
- Delivery framework — timed spine + STAR narration hook
- OOP core concepts
- Guidelines, STAR, pattern instincts
- Pattern details (Strategy, Observer, State, Factory, …)
- Concurrency posts, then classic E2E designs
Worked mini-example: what “good” sounds like
Prompt: “Design a parking lot.” Weak answer jumps to Redis and microservices. Strong Lattice-style answer stays in-process objects for 25 minutes.
# Verbal outline (not production code)
# 1) Requirements: types, assigner, ticket, fee cents, rejects
# 2) Nouns: ParkingLot, ParkingSpot, Ticket, VehicleType
# 3) API: enter / exit
# 4) Trace: full lot + double exit
# 5) Twist: "two gates" → lock around find+occupy
Anti-patterns in LLD interviews
- HLD cosplay — boxes and arrows for a class-design round.
- Pattern laundry list — Strategy/Factory/Observer before state ownership.
- Silent races — never mentioning check-then-act on shared seats/spots.
- Infinite clarifying — minute 12 still asking UI questions.
- Anemic models — bags of getters; all logic in a “ManagerGod”.
Decision checklist before you draw
- Is this LLD (objects) or HLD (services)? Confirm with interviewer if ambiguous.
- What is v1 in scope / explicitly out of scope?
- Which noun mutates, and who protects the invariant?
- What’s the single primary API path you’ll implement?
- What’s one failure path and one concurrency note you’ll volunteer?
Common interview pitfalls
These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.
- Treating LLD as tiny HLD — services first, classes never.
- Never locking a v1; clarifying until time dies.
- Ignoring how companies differ (code-heavy vs design-heavy).
- Staff answer that never mentions evolution or concurrency.
- Memorizing diagrams without ownership language.
Interview script (say this)
Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.
- LLD tests object modeling, APIs, invariants, and communication under time.
- It’s not system design — zoom is classes and methods.
- I’ll use a timed spine: requirements, entities, design, implement, extend.
- Companies vary: some want compiling code, some want clear design talk.
- Interviewers score clarity of ownership and trade-offs.
- Staff+ adds extensibility narrative and concurrency follow-ups.
- This Lattice series goes framework → principles → concurrency → classics.
- I’ll practice one classic timed per day.
Extra verification traces
Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.
Staff-level follow-ups
At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.
- How do you prep? — One timed classic daily + post-mortem pitfalls list.
- Code vs talk? — Ask interviewer format; default to talk+pseudocode unless they want compile.
- Staff bar? — Seams for change + concurrency + evolution story.