LLD — Top Questions & Framework (MAANG SDE-II / L4 / E4)
On this page
- 1. What LLD interviews test in 2026
- 2. Top 20 LLD problems asked at MAANG / Tier-1
- 3. SOLID refresher (Java framing)
- 4. The 12 design patterns you MUST know at L4
- Creational
- Structural
- Behavioral
- 5. LLD answer framework — 45-min budget
- 6. Parking Lot — worked example
- 7. Rate Limiter LLD — Sanjay’s gold story
- 8. Common mistakes / red flags in LLD rounds
- 9. Sources / further reading
Target: Sanjay Naik — 3.5 yrs Java/Spring Boot, Jio Platform Ltd, healthcare microservices. Scope: Low-Level Design rounds at Amazon, Atlassian, Flipkart, Uber, Walmart Labs, Meta, Google.
1. What LLD interviews test in 2026
LLD = “Can you turn a vague problem into clean, extensible Java code in 45 minutes?”
What’s actually graded:
- OOP fluency — encapsulation, inheritance vs composition, polymorphism (favor composition).
- SOLID — interviewer explicitly looks for SRP & OCP violations.
- Design patterns — apply 1–3 patterns, name them out loud, justify the choice.
- UML-lite class diagram — class names, relationships (has-a, is-a), multiplicities.
- Clean Java — interfaces over concretions, enums for fixed sets, immutability for value objects, thread-safety where state is shared.
- Extensibility — “what if we add X?” must require minimal code change (OCP).
- Trade-offs — articulate why you picked Strategy over
if-else, Singleton vs DI, etc.
Round structure by company (2026):
| Company | LLD round | Notes |
|---|---|---|
| Amazon | Mandatory at SDE-II | 60 min, often combined with bar-raiser LP questions |
| Atlassian | Mandatory | Dedicated 60-min “craft” round, code on shared editor |
| Flipkart | Mandatory at SDE-II | Machine coding 90 min — must run |
| Uber | Mandatory | LLD + concurrency emphasis |
| Walmart Labs | Mandatory | LLD + DB schema combined |
| Meta | HLD-heavy | LLD shows up inside coding round as follow-up |
| HLD-heavy | LLD via “design this class” inside coding | |
| Microsoft | Mandatory at SDE-II | Machine coding round |
If targeting Flipkart/Atlassian: practice typing the full thing in IntelliJ in 90 min, not just whiteboarding.
2. Top 20 LLD problems asked at MAANG / Tier-1
Difficulty: E (easy, ~30 min) / M (medium, ~45 min) / H (hard, 60–90 min).
| # | Problem | Difficulty | Dominant Pattern(s) | Frequently Asked At |
|---|---|---|---|---|
| 1 | Parking Lot | M | Strategy (fee), Factory (spot), Singleton | Amazon, Flipkart, Uber, Walmart |
| 2 | Elevator System | H | State, Strategy (scheduling), Observer | Amazon, Uber, Atlassian |
| 3 | Splitwise | M | Strategy (split type), Observer | Amazon, Atlassian, Flipkart |
| 4 | LRU Cache | E | Doubly-LinkedList + HashMap, Template | Meta, Google, Amazon |
| 5 | Logger / Logging Framework | M | Chain of Responsibility, Singleton, Strategy (formatter) | Amazon, Atlassian |
| 6 | Rate Limiter (Sanjay’s project) | M | Strategy (algo), Decorator, Singleton | Amazon, Uber, Stripe, Atlassian |
| 7 | Vending Machine | E | State, Strategy | Amazon, Walmart |
| 8 | Snake & Ladder / Tic-Tac-Toe | E | Strategy (win check), Factory (cell) | Microsoft, Flipkart |
| 9 | Chess | H | Strategy (move per piece), Factory, Command (undo) | Amazon, Google |
| 10 | BookMyShow / Movie Ticket Booking | M | Strategy (seat selection), Observer, Pessimistic Lock | Flipkart, BMS, Atlassian |
| 11 | Library Management | E | Factory, Strategy (search) | Amazon, Walmart |
| 12 | ATM | M | State, Chain of Responsibility (notes dispenser) | Amazon, Walmart |
| 13 | Hotel Booking | M | Strategy (room pricing), Factory | Booking, MMT, Amazon |
| 14 | Cab Booking (Uber-LLD) | H | Strategy (pricing/matching), Observer, State | Uber, Ola, Amazon |
| 15 | Notification System | M | Strategy (channel), Observer, Decorator | Amazon, Atlassian, Meta |
| 16 | File System / Linux ls |
M | Composite, Visitor (search) | Amazon, Google, Microsoft |
| 17 | In-memory Key-Value Store | M | Strategy (eviction), Template, Observer | Amazon, Meta |
| 18 | Stack Overflow / Reddit | H | Strategy (sort), Observer, Composite (comments) | Amazon, Atlassian |
| 19 | Pub-Sub System | H | Observer, Strategy (delivery), Decorator | Amazon, Uber, Meta |
| 20 | Card Game / Deck of Cards | E | Factory, Strategy, Iterator | Amazon, Microsoft |
Recommended order for Sanjay:
- Parking Lot (the canonical warm-up — solve in 30 min cold).
- Rate Limiter (your project — must be airtight).
- Notification System (omnichannel — maps to your work).
- LRU Cache (you’ve built caching at Jio).
- Splitwise + BookMyShow (concurrency + locking).
- Then breadth.
3. SOLID refresher (Java framing)
| Principle | One-liner | Wrong | Right |
|---|---|---|---|
| S — Single Responsibility | One class, one reason to change | User class does DB + email + validation |
Split into UserRepository, UserNotifier, UserValidator |
| O — Open/Closed | Open to extend, closed to modify | if (type=="GST") ... else if (type=="VAT") |
TaxStrategy interface + impls, new tax = new class |
| L — Liskov Substitution | Subtypes must be substitutable | Square extends Rectangle overrides setWidth to also set height — breaks callers |
Keep them sibling impls of Shape |
| I — Interface Segregation | Many small interfaces > one fat one | Worker with work(), eat(), sleep() forces robots to implement eat() |
Split into Workable, Eatable |
| D — Dependency Inversion | Depend on abstractions | class OrderService { MySQLOrderRepo repo = new MySQLOrderRepo(); } |
Inject OrderRepository interface via constructor |
Mnemonic: “Software Objects Live In Dependencies.”
4. The 12 design patterns you MUST know at L4
For each: purpose → when → JDK example → LLD problem where it shines.
Creational
1. Singleton
- Purpose: exactly one instance, global access.
- When: shared registry, config, connection pool. Beware: makes testing harder.
- JDK:
Runtime.getRuntime(). - LLD: Parking Lot entry point, Logger.
- Java idiom (thread-safe, lazy): static holder or
enum.
2. Factory Method
- Purpose: defer instantiation to subclass / method.
- When: client shouldn’t know concrete type.
- JDK:
Calendar.getInstance(). - LLD: ParkingSpot creation, Vehicle creation.
3. Abstract Factory
- Purpose: factory of factories — families of related objects.
- When: cross-platform UI, FHIR vs OpenEHR mapper families (Sanjay’s actual code).
- JDK:
DocumentBuilderFactory. - LLD: Notification channel factories (SMS/Email/Push each with own builder).
4. Builder
- Purpose: stepwise construction of complex immutable objects.
- When: >4 constructor args, optional fields.
- JDK:
StringBuilder,Stream.Builder. - LLD: HTTP request, Pizza/Burger builder, Ticket builder.
5. Prototype
- Purpose: clone existing object instead of new construction.
- When: expensive creation, registry of templates.
- JDK:
Object.clone(),Cloneable. - LLD: Document templates, game piece spawning.
Structural
6. Adapter
- Purpose: convert one interface to another.
- When: integrating 3rd-party / legacy.
- JDK:
Arrays.asList(),InputStreamReader. - LLD: Payment gateways (Stripe vs Razorpay adapters).
7. Decorator
- Purpose: add behavior dynamically without subclassing.
- When: layered behavior (logging + caching + retry).
- JDK:
BufferedReader(new FileReader(...)). - LLD: Notification (add encryption / retry layers), Pizza toppings.
Behavioral
8. Observer
- Purpose: publish/subscribe — notify many on state change.
- When: events, listeners.
- JDK:
java.util.Observer(deprecated),PropertyChangeListener, SpringApplicationEventPublisher. - LLD: Pub-Sub, Notification system, Stock price tracker.
9. Strategy
- Purpose: swap algorithm at runtime via interface.
- When: many ways to do one thing (sort, price, throttle).
- JDK:
Comparator. - LLD: Rate Limiter algo, parking fee, seat selection, eviction policy.
10. Command
- Purpose: encapsulate request as object — supports undo, queue, log.
- When: undo/redo, job queues, macro recording.
- JDK:
Runnable,Callable. - LLD: Chess (undo), text editor, remote control.
11. Template Method
- Purpose: skeleton in base class, steps overridden by subclass.
- When: fixed flow with variable steps.
- JDK:
AbstractList,HttpServlet.service(). - LLD: Logger pipeline, ETL job, payment flow.
12. State
- Purpose: behavior changes with internal state — replace
if-elseon status field. - When: object goes through a finite state machine.
- JDK:
Thread.State(enum, conceptual). - LLD: Vending Machine, ATM, Elevator, Order lifecycle.
5. LLD answer framework — 45-min budget
Treat as a metronome. Talk while you code.
| Phase | Time | What to do | What to say out loud |
|---|---|---|---|
| 1. Clarify requirements | 5 min | Functional + non-functional. Scope cuts. | “Single building or multi? Payment in scope? Concurrent users?” |
| 2. Actors & use-cases | 5 min | Who triggers what. Bullet list. | “Driver enters, system assigns spot, prints ticket, driver exits, pays.” |
| 3. Class diagram | 10 min | Entities + relationships. Whiteboard or comment block. | “ParkingLot HAS-A List<ParkingFloor>; ParkingFloor HAS-A List<ParkingSpot>.” |
| 4. Method signatures + relationships | 10 min | Interfaces first, then concretes. No bodies yet. | “Ticket assignSpot(Vehicle v) returns the ticket, throws NoSpotAvailable.” |
| 5. Code 1–2 key methods | 10 min | The interesting one — usually the algorithm or the locking. | “Let me implement parkVehicle with the spot strategy and synchronized block.” |
| 6. Trade-offs + extensions | 5 min | “If we add X, only Y changes.” | “Adding EV spot = new enum + new strategy impl. No existing class changes.” |
Red flags that eat your clock: bikeshedding on enum names, drawing UML too pretty, implementing getters/setters.
6. Parking Lot — worked example
Patterns used: Singleton (entry), Factory (spot creation), Strategy (fee), Enum (types).
// ----- Enums -----
public enum VehicleType { MOTORCYCLE, CAR, TRUCK }
public enum SpotType { COMPACT, REGULAR, LARGE }
public enum TicketStatus { ACTIVE, PAID, LOST }
// ----- Vehicle hierarchy -----
public abstract class Vehicle {
private final String licensePlate;
private final VehicleType type;
protected Vehicle(String plate, VehicleType type) {
this.licensePlate = plate;
this.type = type;
}
public VehicleType getType() { return type; }
public String getLicensePlate() { return licensePlate; }
}
public class Car extends Vehicle { public Car(String p) { super(p, VehicleType.CAR); } }
public class Motorcycle extends Vehicle { public Motorcycle(String p) { super(p, VehicleType.MOTORCYCLE); } }
public class Truck extends Vehicle { public Truck(String p) { super(p, VehicleType.TRUCK); } }
// ----- Spot -----
public class ParkingSpot {
private final String id;
private final SpotType type;
private Vehicle occupant; // null when free
public ParkingSpot(String id, SpotType type) { this.id = id; this.type = type; }
public boolean isFree() { return occupant == null; }
public boolean canFit(Vehicle v) { /* truck→LARGE; car→REGULAR/LARGE; moto→any */ return true; }
public synchronized boolean assign(Vehicle v) {
if (occupant != null) return false;
occupant = v; return true;
}
public synchronized void release() { occupant = null; }
public SpotType getType() { return type; }
public String getId() { return id; }
}
// ----- Floor -----
public class ParkingFloor {
private final int floorNumber;
private final List<ParkingSpot> spots;
public ParkingFloor(int n, List<ParkingSpot> spots) { this.floorNumber = n; this.spots = spots; }
public Optional<ParkingSpot> findSpot(Vehicle v) {
return spots.stream().filter(s -> s.isFree() && s.canFit(v)).findFirst();
}
}
// ----- Ticket -----
public class Ticket {
private final String id;
private final String licensePlate;
private final String spotId;
private final Instant entryTime;
private Instant exitTime;
private TicketStatus status = TicketStatus.ACTIVE;
private BigDecimal amount;
// ctor + getters/setters
}
// ----- Strategy: fee -----
public interface FeeStrategy {
BigDecimal calculate(Ticket t, VehicleType type);
}
public class HourlyFeeStrategy implements FeeStrategy {
public BigDecimal calculate(Ticket t, VehicleType type) { /* hours * rate(type) */ return null; }
}
public class FlatFeeStrategy implements FeeStrategy {
public BigDecimal calculate(Ticket t, VehicleType type) { return new BigDecimal("100"); }
}
// ----- Factory: spot creation -----
public class SpotFactory {
public static ParkingSpot create(String id, SpotType type) {
return new ParkingSpot(id, type); // can branch on type for sub-classes
}
}
// ----- Singleton: ParkingLot -----
public final class ParkingLot {
private static volatile ParkingLot INSTANCE;
private final List<ParkingFloor> floors;
private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
private FeeStrategy feeStrategy;
private ParkingLot(List<ParkingFloor> floors, FeeStrategy fee) {
this.floors = floors; this.feeStrategy = fee;
}
public static ParkingLot getInstance(List<ParkingFloor> floors, FeeStrategy fee) {
if (INSTANCE == null) {
synchronized (ParkingLot.class) {
if (INSTANCE == null) INSTANCE = new ParkingLot(floors, fee);
}
}
return INSTANCE;
}
public Ticket parkVehicle(Vehicle v) {
for (ParkingFloor f : floors) {
Optional<ParkingSpot> spot = f.findSpot(v);
if (spot.isPresent() && spot.get().assign(v)) {
Ticket t = new Ticket(/* uuid */, v.getLicensePlate(), spot.get().getId(), Instant.now());
activeTickets.put(t.getId(), t);
return t;
}
}
throw new IllegalStateException("No spot available");
}
public BigDecimal exitVehicle(String ticketId) {
Ticket t = activeTickets.remove(ticketId);
if (t == null) throw new IllegalArgumentException("Unknown ticket");
t.setExitTime(Instant.now());
BigDecimal amount = feeStrategy.calculate(t, /* lookup type */ VehicleType.CAR);
// free the spot lookup omitted
return amount;
}
public void setFeeStrategy(FeeStrategy fs) { this.feeStrategy = fs; } // hot-swap pricing
}
How the patterns interact:
ParkingLotis the Singleton orchestrator.SpotFactorybuilds spots — caller doesn’tnew ParkingSpot(...)directly; swap to a sub-class hierarchy if needed.FeeStrategyis hot-swappable — switch fromHourlyFeeStrategytoFlatFeeStrategywithout touchingParkingLot.- Concurrency:
synchronizedonassign/releaseper spot,ConcurrentHashMapfor tickets.
Extensions to mention out loud: EV spots (new SpotType), reserved spots (new assign precondition), surge pricing (new FeeStrategy).
7. Rate Limiter LLD — Sanjay’s gold story
This maps directly to your AI Rate Limiter + omnichannel multi-tenant work. Lead with this. Patterns: Strategy (algorithm), Factory (algo by config), Singleton (registry), Decorator (metrics/logging wrapper).
Requirements to clarify out loud:
- Per-tenant, per-API key, per-endpoint limits? (Sanjay: yes — multi-tenant).
- Single-node or distributed? (Distributed → Redis-backed counter).
- Algorithms supported? (Token bucket, sliding window, fixed window).
- Action on limit breach? (Reject 429, queue, soft-throttle).
- SLA: how strict on cluster-wide accuracy vs latency?
// ----- Core abstraction -----
public interface RateLimiter {
/** @return true if request allowed; false if throttled */
boolean tryAcquire(String tenantId, String key, int permits);
}
// ----- Per-tenant config -----
public class RateLimitConfig {
private final int capacity; // max tokens / window size
private final int refillPerSecond; // tokens added per sec (TB)
private final Duration window; // window duration (SW/FW)
private final Algorithm algorithm;
// ctor + getters
}
public enum Algorithm { TOKEN_BUCKET, SLIDING_WINDOW, FIXED_WINDOW }
// ----- Redis-backed counter store -----
public interface CounterStore {
long incrementAndGet(String key, Duration ttl);
long get(String key);
boolean compareAndSet(String key, long expected, long updated);
Long evalLua(String script, List<String> keys, List<String> args); // atomic ops
}
public class RedisCounterStore implements CounterStore { /* Jedis/Lettuce impl */ }
// ----- Strategy 1: Token Bucket -----
public class TokenBucketLimiter implements RateLimiter {
private final CounterStore store;
private final Function<String, RateLimitConfig> configResolver; // per-tenant
public TokenBucketLimiter(CounterStore s, Function<String, RateLimitConfig> r) {
this.store = s; this.configResolver = r;
}
@Override
public boolean tryAcquire(String tenantId, String key, int permits) {
RateLimitConfig cfg = configResolver.apply(tenantId);
String redisKey = "rl:tb:" + tenantId + ":" + key;
// Lua script: atomically refill based on elapsed time, then decrement by `permits`
Long allowed = store.evalLua(REFILL_AND_TAKE_LUA,
List.of(redisKey),
List.of(String.valueOf(cfg.getCapacity()),
String.valueOf(cfg.getRefillPerSecond()),
String.valueOf(permits),
String.valueOf(Instant.now().toEpochMilli())));
return allowed != null && allowed == 1L;
}
private static final String REFILL_AND_TAKE_LUA = "..."; // see notes
}
// ----- Strategy 2: Sliding Window Log -----
public class SlidingWindowLimiter implements RateLimiter {
private final CounterStore store;
private final Function<String, RateLimitConfig> configResolver;
public SlidingWindowLimiter(CounterStore s, Function<String, RateLimitConfig> r) {
this.store = s; this.configResolver = r;
}
@Override
public boolean tryAcquire(String tenantId, String key, int permits) {
// ZADD timestamp; ZREMRANGEBYSCORE older than now-window; ZCARD; compare to capacity
return true; // skeleton
}
}
// ----- Strategy 3: Fixed Window -----
public class FixedWindowLimiter implements RateLimiter {
private final CounterStore store;
private final Function<String, RateLimitConfig> configResolver;
public FixedWindowLimiter(CounterStore s, Function<String, RateLimitConfig> r) {
this.store = s; this.configResolver = r;
}
@Override
public boolean tryAcquire(String tenantId, String key, int permits) {
RateLimitConfig cfg = configResolver.apply(tenantId);
long bucket = System.currentTimeMillis() / cfg.getWindow().toMillis();
String redisKey = "rl:fw:" + tenantId + ":" + key + ":" + bucket;
long count = store.incrementAndGet(redisKey, cfg.getWindow());
return count <= cfg.getCapacity();
}
}
// ----- Factory -----
public class RateLimiterFactory {
public static RateLimiter create(Algorithm algo, CounterStore store,
Function<String, RateLimitConfig> resolver) {
return switch (algo) {
case TOKEN_BUCKET -> new TokenBucketLimiter(store, resolver);
case SLIDING_WINDOW -> new SlidingWindowLimiter(store, resolver);
case FIXED_WINDOW -> new FixedWindowLimiter(store, resolver);
};
}
}
// ----- Decorator: metrics -----
public class MetricsRateLimiter implements RateLimiter {
private final RateLimiter delegate;
private final MeterRegistry metrics;
public MetricsRateLimiter(RateLimiter d, MeterRegistry m) { this.delegate = d; this.metrics = m; }
@Override
public boolean tryAcquire(String tenantId, String key, int permits) {
boolean ok = delegate.tryAcquire(tenantId, key, permits);
metrics.counter("rate_limiter", "tenant", tenantId, "result", ok ? "allowed" : "denied").increment();
return ok;
}
}
// ----- Singleton registry of per-tenant configs -----
public final class TenantConfigRegistry {
private static final TenantConfigRegistry INSTANCE = new TenantConfigRegistry();
private final Map<String, RateLimitConfig> configs = new ConcurrentHashMap<>();
private TenantConfigRegistry() {}
public static TenantConfigRegistry getInstance() { return INSTANCE; }
public RateLimitConfig get(String tenantId) { return configs.get(tenantId); }
public void put(String tenantId, RateLimitConfig cfg) { configs.put(tenantId, cfg); }
}
Talking points to drop in the interview:
- Why Redis-backed? Distributed across N pods — local counters are wrong under load balancer.
- Why Lua for token bucket? Refill + decrement must be atomic; round-trip is one shot.
- Why Strategy? AI workloads (LLM inference) need different shapes — bursty token bucket for chat, fixed window for batch.
- Multi-tenant? Key namespacing
rl:<algo>:<tenant>:<endpoint>, config resolver pulls per-tenant limits from DB / cached. - Failure mode? If Redis is down — fail open or closed? Configurable per tenant.
This is your STAR story anchor: “At Jio, I built an AI Rate Limiter for our omnichannel platform. We had multi-tenant SaaS with wildly different SLAs — used Strategy to swap algorithms per tenant, Redis Lua for atomicity, and a decorator chain for metrics and circuit breaking.”
8. Common mistakes / red flags in LLD rounds
- Over-engineering — 14 patterns for Tic-Tac-Toe. Use the minimum; name 1–2 patterns deliberately.
- Missing abstractions — no interfaces, just concrete classes wired together. Always start with the interface.
-
instanceofchains —if (v instanceof Car) ... else if (v instanceof Truck)— replace with polymorphism or Strategy. - God classes —
ParkingLotdoing parking, billing, reporting, notifications. Split by SRP. - No thread-safety where it matters — two threads grab same spot, double-charging, lost updates. Mention
synchronized/ConcurrentHashMap/ Redis Lua even if you don’t fully implement. - Ignoring extensibility — when asked “what if we add EV?” don’t say “change the enum and the if-else.” Show OCP-clean extension.
- Exposing mutable internals — returning
List<ParkingSpot>directly. Wrap inCollections.unmodifiableListor expose query methods. - Mixing concerns — business logic in controllers, DB calls inside domain objects. Keep layers clean.
- Skipping enums — strings for status (
"ACTIVE","PAID"). Use enums. - Not asking clarifying questions — diving into code at minute 1. Spend the first 5.
- No trade-offs spoken — silent coding. Narrate every design choice.
- Forgetting equals/hashCode for value objects used as map keys.
- Returning null instead of
Optionalfor “not found” cases.
9. Sources / further reading
Companion files in this folder:
../01-DSA/top-questions.md— pattern-based DSA prep../02-HLD/top-questions.md— system design top questions../04-Behavioral/top-questions.md— STAR stories & LP
Prep cadence suggestion:
- Week 1 — Parking Lot + Rate Limiter (cold-solve in 45 min, twice each)
- Week 2 — Notification System + LRU Cache + Splitwise
- Week 3 — BookMyShow + Elevator + File System
- Week 4 — Mock with peer on shared IntelliJ, time-boxed 45 min