- Learn ten patterns deeply: Singleton, Factory, Builder (creational), Adapter, Decorator, Proxy (structural), Strategy, Observer, State, Command (behavioural).
- Every pattern answers one question: what is likely to change, and how do I isolate it? Lead with the change, not the pattern name.
- Each section below gives minimal runnable Java, a real use case (payments, notifications, vending machines), and the follow-up interviewers ask.
- The confusing pairs (Strategy vs State, Decorator vs Proxy, Adapter vs Facade) are where candidates lose marks. They are covered at the end.
Design patterns are not an exam topic you memorise from a 1994 book. In an LLD or machine coding round they are the vocabulary you use to explain why your code is shaped the way it is. If you say "pricing rules change every quarter, so I put them behind a PricingStrategy interface", the interviewer hears both a pattern and a design reason. If you say "I used Strategy pattern" with nothing behind it, they hear a buzzword.
This guide covers the ten patterns that do most of the work in real interviews. For each one you get the problem it solves, compact Java you can reproduce on a whiteboard, where it shows up in actual LLD problems, and the follow-up questions that separate a memoriser from a designer. If OOP basics are shaky, read our OOPs interview questions guide first; if the SOLID principles are, read SOLID principles with examples, because most patterns are SOLID applied to a recurring situation.
Why Interviewers Ask About Design Patterns
At Indian product companies such as Flipkart, Swiggy, PhonePe, Razorpay and Uber, the LLD or machine coding round typically gives you 60 to 120 minutes to build something like a parking lot, a Splitwise clone or a rate limiter. The interviewer is grading three things: does it work, is it readable, and is it extensible. Extensibility is where patterns come in. The classic follow-up is "now add a new vehicle type / payment method / notification channel. What do you change?" If the honest answer is "add one class and touch nothing else", you used the right pattern, whether or not you named it.
Theory questions also appear, especially for freshers and in Java-heavy interviews: "write a thread-safe Singleton", "difference between Factory and Abstract Factory", "where does the JDK use Decorator?". Those have crisp answers, and you will find them below.
Design Patterns Cheat Sheet: The Ten at a Glance
| Pattern | Type | What varies / problem solved | Classic interview use |
|---|---|---|---|
| Singleton | Creational | Exactly one shared instance | Config, logger, parking lot instance |
| Factory | Creational | Which concrete class to instantiate | Vehicle, payment method, notification channel creation |
| Builder | Creational | Many optional constructor params | HTTP request, Pizza/Order, immutable DTOs |
| Strategy | Behavioural | Interchangeable algorithms | Pricing, fee calculation, split types in Splitwise |
| Observer | Behavioural | Who reacts to an event | Notifications, stock price alerts, order status |
| Decorator | Structural | Add responsibilities at runtime | Pizza toppings, Java I/O streams, middleware |
| Adapter | Structural | Incompatible interfaces | Third-party payment gateway SDKs |
| State | Behavioural | Behaviour depends on state | Vending machine, ATM, order lifecycle |
| Command | Behavioural | Requests as objects (undo, queue) | Text editor undo, remote control, job queue |
| Proxy | Structural | Control access to a real object | Caching, lazy loading, auth checks, rate limits |
1. Singleton Pattern: Thread-Safe Versions That Actually Work
Intent: guarantee a class has exactly one instance and give a global access point to it. It is the most asked pattern for freshers, mainly because the thread-safe version has a real trap.
The naive version and why it breaks
The lazy if (instance == null) instance = new X(); version is broken under concurrency: two threads can both see null and create two instances. Making getInstance() fully synchronized fixes correctness but takes a lock on every call. The interview answer is double-checked locking with volatile, or better, the holder idiom or an enum.
// Option A: double-checked locking. volatile is mandatory.
public final class ConfigRegistry {
private static volatile ConfigRegistry instance;
private final java.util.Map<String, String> props = new java.util.concurrent.ConcurrentHashMap<>();
private ConfigRegistry() { }
public static ConfigRegistry getInstance() {
ConfigRegistry local = instance; // one volatile read on the fast path
if (local == null) {
synchronized (ConfigRegistry.class) {
local = instance;
if (local == null) {
local = new ConfigRegistry();
instance = local;
}
}
}
return local;
}
public String get(String k) { return props.get(k); }
public void set(String k, String v) { props.put(k, v); }
}
// Option B: initialization-on-demand holder. Lazy and thread-safe via class loading.
final class IdGenerator {
private IdGenerator() { }
private static class Holder { static final IdGenerator INSTANCE = new IdGenerator(); }
public static IdGenerator getInstance() { return Holder.INSTANCE; }
}
// Option C: enum singleton. Serialization- and reflection-safe.
enum AppLogger {
INSTANCE;
public void log(String msg) { System.out.println("[LOG] " + msg); }
}
Why volatile matters
instance = new ConfigRegistry() is not atomic: allocate memory, run the constructor, publish the reference. Without volatile, the JVM may reorder so another thread sees a non-null reference to a partially constructed object. volatile establishes a happens-before edge that forbids this. Saying this one sentence clearly is often the difference between "knows Singleton" and "understands Singleton".
Follow-ups interviewers ask
- How can a Singleton be broken? Reflection (calling the private constructor via
setAccessible(true)), serialization (deserializing creates a new object unless you addreadResolve()), cloning, and multiple class loaders. The enum version is immune to the first two. - Is Singleton good design? Answer honestly: it is global state, hides dependencies, and hurts testability. In Spring, beans are singleton-scoped by default but are injected, which keeps the benefit without the static coupling.
- Singleton in a distributed system? It is one per JVM (per class loader), not one per cluster. A cluster-wide single instance needs leader election or a distributed lock.
2. Factory Pattern: Simple Factory, Factory Method, Abstract Factory
Intent: move the decision "which concrete class do I instantiate?" out of client code. The client depends on an interface; the factory owns the new. This directly serves the Open/Closed Principle: adding a new type touches the factory, not the 30 call sites.
interface Notifier { void send(String to, String msg); }
class EmailNotifier implements Notifier {
public void send(String to, String msg) { System.out.println("Email to " + to + ": " + msg); }
}
class SmsNotifier implements Notifier {
public void send(String to, String msg) { System.out.println("SMS to " + to + ": " + msg); }
}
class WhatsAppNotifier implements Notifier {
public void send(String to, String msg) { System.out.println("WhatsApp to " + to + ": " + msg); }
}
enum Channel { EMAIL, SMS, WHATSAPP }
// Simple factory: one static method, switch on a parameter.
final class NotifierFactory {
private static final java.util.Map<Channel, java.util.function.Supplier<Notifier>> REGISTRY =
new java.util.EnumMap<>(Channel.class);
static {
REGISTRY.put(Channel.EMAIL, EmailNotifier::new);
REGISTRY.put(Channel.SMS, SmsNotifier::new);
REGISTRY.put(Channel.WHATSAPP, WhatsAppNotifier::new);
}
static Notifier create(Channel c) {
java.util.function.Supplier<Notifier> s = REGISTRY.get(c);
if (s == null) throw new IllegalArgumentException("Unknown channel " + c);
return s.get();
}
}
The registry-of-suppliers version is worth showing because it removes the giant switch: a new channel is one new class plus one registry line. Interviewers like this.
Factory Method vs Abstract Factory
Factory Method is the GoF version: a superclass defines createX() as an overridable method and subclasses decide the product. Example: an abstract Logistics class with createTransport(), overridden by RoadLogistics (returns a truck) and SeaLogistics (returns a ship). Abstract Factory creates a family of related objects, for example a UiFactory with createButton() and createCheckbox(), implemented by DarkThemeFactory and LightThemeFactory, so a dark button is never mixed with a light checkbox.
Where it shows up in LLD rounds
Parking lot (creating spots or vehicles by type), payment systems (UPI, card, netbanking processors), notification services, and parsers (choose a JSON/CSV/XML parser by file extension). In the JDK: Calendar.getInstance(), NumberFormat.getInstance(), List.of().
3. Builder Pattern: Readable Construction of Complex Objects
Intent: construct an object step by step when it has many parameters, most optional, and ideally make it immutable. The alternative, a "telescoping constructor" with eight positional arguments, invites bugs like swapping two int parameters.
public final class HttpRequest {
private final String url; // required
private final String method; // optional, default GET
private final java.util.Map<String, String> headers;
private final String body;
private final int timeoutMs;
private HttpRequest(Builder b) {
this.url = b.url;
this.method = b.method;
this.headers = java.util.Collections.unmodifiableMap(new java.util.HashMap<>(b.headers));
this.body = b.body;
this.timeoutMs = b.timeoutMs;
}
public static Builder builder(String url) { return new Builder(url); }
public static final class Builder {
private final String url;
private String method = "GET";
private final java.util.Map<String, String> headers = new java.util.HashMap<>();
private String body;
private int timeoutMs = 5000;
private Builder(String url) { this.url = java.util.Objects.requireNonNull(url); }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String b) { this.body = b; return this; }
public Builder timeoutMs(int t) { this.timeoutMs = t; return this; }
public HttpRequest build() {
if (("POST".equals(method) || "PUT".equals(method)) && body == null)
throw new IllegalStateException(method + " requires a body");
if (timeoutMs <= 0) throw new IllegalStateException("timeout must be positive");
return new HttpRequest(this);
}
}
@Override public String toString() { return method + " " + url + " headers=" + headers; }
}
// Usage:
// HttpRequest r = HttpRequest.builder("https://api.example.com/orders")
// .method("POST").header("Authorization", "Bearer x").body("{}").build();
Two details interviewers notice: required parameters go into the builder's constructor (so you cannot forget them), and cross-field validation lives in build(). Mention that Lombok's @Builder generates this, and that StringBuilder and Stream.Builder are JDK relatives.
4. Strategy Pattern: Swappable Algorithms Without if-else Chains
Intent: define a family of algorithms, encapsulate each one, and make them interchangeable. This is the single most useful pattern in machine coding rounds because almost every problem has a rule that varies: fare calculation, split type, eviction policy, ranking.
import java.util.*;
interface SplitStrategy {
Map<String, Double> split(double amount, List<String> users, List<Double> params);
}
class EqualSplit implements SplitStrategy {
public Map<String, Double> split(double amount, List<String> users, List<Double> p) {
Map<String, Double> out = new LinkedHashMap<>();
double share = Math.round(amount * 100.0 / users.size()) / 100.0;
for (String u : users) out.put(u, share);
// put the rounding remainder on the first user so the total matches exactly
double diff = Math.round((amount - share * users.size()) * 100.0) / 100.0;
out.merge(users.get(0), diff, Double::sum);
return out;
}
}
class PercentSplit implements SplitStrategy {
public Map<String, Double> split(double amount, List<String> users, List<Double> pct) {
double total = pct.stream().mapToDouble(Double::doubleValue).sum();
if (Math.abs(total - 100.0) > 1e-9) throw new IllegalArgumentException("percent must sum to 100");
Map<String, Double> out = new LinkedHashMap<>();
for (int i = 0; i < users.size(); i++) out.put(users.get(i), amount * pct.get(i) / 100.0);
return out;
}
}
class ExpenseService {
private final Map<String, SplitStrategy> strategies = Map.of(
"EQUAL", new EqualSplit(), "PERCENT", new PercentSplit());
Map<String, Double> addExpense(String type, double amt, List<String> users, List<Double> p) {
SplitStrategy s = strategies.get(type);
if (s == null) throw new IllegalArgumentException("unsupported split " + type);
return s.split(amt, users, p);
}
}
This is the core of the Splitwise LLD. Adding an "exact amount" split is one new class. Note the rounding handling: ₹100 split three ways is 33.33 + 33.33 + 33.34, not three times 33.33. Interviewers deliberately test that.
In modern Java, a strategy with one method is just a functional interface, so Comparator passed to Collections.sort is the most famous Strategy in the JDK. Say so; it shows you see patterns in code you already use.
5. Observer Pattern: Event Notifications and Pub/Sub
Intent: when one object (the subject) changes, all registered dependents (observers) are notified, without the subject knowing their concrete types. Think "order status changed" in a Swiggy-like app: the customer app, the restaurant dashboard, the delivery partner and an analytics pipeline all care, and the list keeps growing.
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
enum OrderStatus { PLACED, ACCEPTED, PICKED_UP, DELIVERED }
interface OrderObserver { void onStatusChange(String orderId, OrderStatus status); }
class Order {
private final String id;
private OrderStatus status = OrderStatus.PLACED;
private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();
Order(String id) { this.id = id; }
void subscribe(OrderObserver o) { observers.add(o); }
void unsubscribe(OrderObserver o) { observers.remove(o); }
void updateStatus(OrderStatus next) {
this.status = next;
for (OrderObserver o : observers) {
try { o.onStatusChange(id, next); }
catch (RuntimeException e) { System.err.println("observer failed: " + e.getMessage()); }
}
}
}
class CustomerPush implements OrderObserver {
public void onStatusChange(String id, OrderStatus s) { System.out.println("Push: order " + id + " is " + s); }
}
class AnalyticsSink implements OrderObserver {
public void onStatusChange(String id, OrderStatus s) { System.out.println("Analytics event " + id + "," + s); }
}
Details that earn points
- One failing observer should not stop the others, hence the try/catch around each notification.
- Concurrent modification: an observer that unsubscribes itself during notification would throw
ConcurrentModificationExceptionon anArrayList.CopyOnWriteArrayListfixes it and suits read-heavy listener lists. - Memory leaks: forgotten subscriptions keep objects alive (the "lapsed listener" problem). Always provide unsubscribe.
- Sync vs async: in-process Observer is synchronous. At system scale the same idea becomes a message broker like Kafka, which is how you bridge from LLD to HLD (see Kafka vs RabbitMQ vs SQS).
6. Decorator Pattern: Adding Behaviour Without Subclass Explosion
Intent: attach responsibilities to an object dynamically by wrapping it in another object that implements the same interface. The textbook motivation: a coffee shop with 4 base drinks and 6 add-ons would need dozens of subclasses (MochaWithWhipAndSoy...). Decorators compose instead.
interface Pizza { String description(); int priceInRupees(); }
class Margherita implements Pizza {
public String description() { return "Margherita"; }
public int priceInRupees() { return 249; }
}
abstract class ToppingDecorator implements Pizza {
protected final Pizza inner;
ToppingDecorator(Pizza inner) { this.inner = inner; }
}
class ExtraCheese extends ToppingDecorator {
ExtraCheese(Pizza p) { super(p); }
public String description() { return inner.description() + " + extra cheese"; }
public int priceInRupees() { return inner.priceInRupees() + 60; }
}
class Jalapeno extends ToppingDecorator {
Jalapeno(Pizza p) { super(p); }
public String description() { return inner.description() + " + jalapeno"; }
public int priceInRupees() { return inner.priceInRupees() + 40; }
}
// new Jalapeno(new ExtraCheese(new Margherita())) -> 249 + 60 + 40 = 349
The JDK example everyone should cite is java.io: new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"))). Each layer adds behaviour (buffering, decoding) to the same stream abstraction. Web middleware (logging, auth, compression wrapped around a handler) is the same idea.
Trade-off to mention: many small wrapper objects make debugging and stack traces harder, and the order of wrapping can matter (compress-then-encrypt is not the same as encrypt-then-compress).
7. Adapter Pattern: Making Incompatible Interfaces Work Together
Intent: convert the interface of a class into the one your code expects. It is how you integrate third-party SDKs without letting their shape leak into your domain. A realistic India-flavoured example: your checkout code uses a PaymentGateway interface with amounts in rupees, but a vendor SDK takes amounts in paise and returns its own response type.
// Your domain interface
interface PaymentGateway {
boolean charge(String orderId, double amountInRupees);
}
// Third-party SDK you cannot modify (shape is illustrative)
class VendorPayClient {
VendorResponse createPayment(long amountInPaise, String referenceId, String currency) {
return new VendorResponse("captured");
}
}
class VendorResponse {
final String state;
VendorResponse(String state) { this.state = state; }
}
// Adapter: implements your interface, delegates to the SDK
class VendorPayAdapter implements PaymentGateway {
private final VendorPayClient client;
VendorPayAdapter(VendorPayClient client) { this.client = client; }
@Override
public boolean charge(String orderId, double amountInRupees) {
long paise = Math.round(amountInRupees * 100);
VendorResponse r = client.createPayment(paise, orderId, "INR");
return "captured".equals(r.state);
}
}
Switching vendors later means writing another adapter; checkout code never changes. In the JDK, Arrays.asList() (array to List) and InputStreamReader (byte stream to character stream) are adapters.
8. State Pattern: The Vending Machine and ATM Favourite
Intent: let an object change its behaviour when its internal state changes, by delegating to a state object. If your class has methods that all begin with switch (state), State pattern is the cure. It is the expected design in the vending machine LLD and the ATM LLD.
interface VendingState {
void insertCoin(VendingMachine m, int amount);
void selectItem(VendingMachine m, String code);
void dispense(VendingMachine m);
}
class VendingMachine {
private VendingState state = new IdleState();
int balance;
String selected;
final java.util.Map<String, Integer> prices = new java.util.HashMap<>(java.util.Map.of("A1", 20, "B2", 35));
void setState(VendingState s) { this.state = s; }
void insertCoin(int amt) { state.insertCoin(this, amt); }
void selectItem(String code) { state.selectItem(this, code); }
void dispense() { state.dispense(this); }
}
class IdleState implements VendingState {
public void insertCoin(VendingMachine m, int amt) { m.balance += amt; m.setState(new HasMoneyState()); }
public void selectItem(VendingMachine m, String c) { System.out.println("Insert money first"); }
public void dispense(VendingMachine m) { System.out.println("Nothing to dispense"); }
}
class HasMoneyState implements VendingState {
public void insertCoin(VendingMachine m, int amt) { m.balance += amt; }
public void selectItem(VendingMachine m, String c) {
Integer price = m.prices.get(c);
if (price == null) { System.out.println("Invalid item"); return; }
if (m.balance < price) { System.out.println("Insufficient balance"); return; }
m.selected = c;
m.setState(new DispensingState());
m.dispense();
}
public void dispense(VendingMachine m) { System.out.println("Select an item first"); }
}
class DispensingState implements VendingState {
public void insertCoin(VendingMachine m, int amt) { System.out.println("Busy, please wait"); }
public void selectItem(VendingMachine m, String c) { System.out.println("Busy, please wait"); }
public void dispense(VendingMachine m) {
int change = m.balance - m.prices.get(m.selected);
System.out.println("Dispensed " + m.selected + ", change " + change);
m.balance = 0; m.selected = null;
m.setState(new IdleState());
}
}
Each state handles every action, so illegal transitions are explicit ("Busy, please wait") instead of being silent bugs. Adding a MaintenanceState touches no existing state class except where a transition into it happens. A common improvement is to make states stateless singletons so you do not allocate on every transition.
9. Command Pattern: Undo, Redo and Queued Requests
Intent: wrap a request as an object, so you can queue it, log it, retry it or undo it. Undo/redo in a text editor is the canonical interview prompt.
import java.util.ArrayDeque;
import java.util.Deque;
interface Command { void execute(); void undo(); }
class TextDocument {
private final StringBuilder text = new StringBuilder();
void insert(int pos, String s) { text.insert(pos, s); }
void delete(int pos, int len) { text.delete(pos, pos + len); }
String substring(int pos, int len) { return text.substring(pos, pos + len); }
public String toString() { return text.toString(); }
}
class InsertCommand implements Command {
private final TextDocument doc; private final int pos; private final String s;
InsertCommand(TextDocument d, int pos, String s) { this.doc = d; this.pos = pos; this.s = s; }
public void execute() { doc.insert(pos, s); }
public void undo() { doc.delete(pos, s.length()); }
}
class DeleteCommand implements Command {
private final TextDocument doc; private final int pos, len; private String removed;
DeleteCommand(TextDocument d, int pos, int len) { this.doc = d; this.pos = pos; this.len = len; }
public void execute() { removed = doc.substring(pos, len); doc.delete(pos, len); }
public void undo() { doc.insert(pos, removed); }
}
class Editor {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
void run(Command c) { c.execute(); undoStack.push(c); redoStack.clear(); }
void undo() { if (!undoStack.isEmpty()) { Command c = undoStack.pop(); c.undo(); redoStack.push(c); } }
void redo() { if (!redoStack.isEmpty()) { Command c = redoStack.pop(); c.execute(); undoStack.push(c); } }
}
Key points: a new command clears the redo stack; DeleteCommand must remember what it removed to undo correctly. Beyond editors, Command underlies job queues (a Runnable submitted to an ExecutorService is a command), transactional logs and macro recording.
10. Proxy Pattern: Caching, Lazy Loading and Access Control
Intent: provide a stand-in that controls access to a real object. It has the same interface; the client cannot tell. Common flavours: virtual proxy (lazy creation of something expensive), protection proxy (authorization), caching proxy, and remote proxy (RPC stubs).
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
interface ProductCatalog { String getProduct(String id); }
class DbProductCatalog implements ProductCatalog {
public String getProduct(String id) {
System.out.println("DB hit for " + id); // pretend this is a 30 ms query
return "Product-" + id;
}
}
class CachingCatalogProxy implements ProductCatalog {
private final ProductCatalog real;
private final Map<String, String> cache = new ConcurrentHashMap<>();
CachingCatalogProxy(ProductCatalog real) { this.real = real; }
public String getProduct(String id) {
return cache.computeIfAbsent(id, real::getProduct);
}
}
Real-world proxies you should name: Spring's @Transactional and @Cacheable work by wrapping your bean in a generated proxy (which is why calling a @Transactional method from inside the same class bypasses the transaction, a favourite Spring interview trap); Hibernate lazy-loaded entities are proxies; java.lang.reflect.Proxy builds dynamic proxies at runtime. At HLD level, an API gateway is a protection plus caching proxy for your services.
Commonly Confused Design Patterns (and How to Tell Them Apart)
Interviewers love asking for differences because the class diagrams of several patterns are almost identical. The distinction is always in intent.
| Pair | Same structure? | The real difference |
|---|---|---|
| Strategy vs State | Yes: context holds an interface reference | Strategy is chosen by the client and strategies are independent. States change themselves and know their successors. |
| Decorator vs Proxy | Yes: wrapper with same interface | Decorator adds behaviour and is stacked by the client. Proxy controls access, and usually manages the real object's lifecycle itself. |
| Adapter vs Decorator | Both wrap | Adapter changes the interface. Decorator keeps the interface and adds behaviour. |
| Adapter vs Facade | Both hide something | Adapter makes one interface match another. Facade gives a simpler front over a whole subsystem (e.g., a CheckoutFacade over inventory, pricing, payment). |
| Factory vs Builder | Both create | Factory chooses which class in one call. Builder assembles one complex object step by step. |
| Observer vs Pub/Sub | Similar idea | Observer: subject knows its observers directly, usually in-process. Pub/Sub: a broker decouples publishers and subscribers completely, often across services. |
| Command vs Strategy | Both encapsulate behaviour | Command encapsulates a request (what to do, with its arguments, often undoable). Strategy encapsulates how to do a computation. |
How to Use Design Patterns in an LLD or Machine Coding Round
Knowing ten patterns is half the job. The other half is deploying them at the right moment in a timed round. Here is the sequence I recommend in mock interviews.
- Clarify requirements and list what will change. For a parking lot: vehicle types, spot types, pricing rules, payment modes. Write these down; each is a candidate for a pattern.
- Model entities first, patterns second. Get
ParkingLot,Floor,Spot,Ticketright before you think about abstractions. Patterns sit on top of a correct domain model. - Apply a pattern only where a listed change lives. Pricing varies: Strategy. Spot allocation varies (nearest, by floor): Strategy. Display boards update on every entry and exit: Observer. Ticket creation with optional fields: Builder.
- Name it after you justify it. "Because pricing differs for weekends and EVs, I'll inject a
PricingStrategy; this is Strategy pattern." That ordering sounds like engineering, not recitation. - Prove extensibility with the follow-up. When asked to add a feature, point at exactly which new class you would add and confirm no existing class changes.
Red flags interviewers report
- Pattern stuffing: a Singleton, an Abstract Factory and a Visitor in a 90-minute Snake and Ladder game. Over-engineering is marked down as much as under-engineering.
- Singletons everywhere as a substitute for passing dependencies. It makes the code untestable, and they will ask how you would unit test it.
- Wrong pattern names: calling a wrapper "Decorator" when it changes the interface. Better to describe the design without a name than mislabel it.
- Not running the code. In machine coding rounds at companies like Flipkart and Swiggy, working code with a demo driver matters more than a perfect class diagram. See our machine coding round guide.
Which LLD problems need which patterns
| LLD problem | Patterns that naturally fit |
|---|---|
| Parking lot | Strategy (pricing, spot allocation), Factory (vehicles/spots), Observer (display boards), Singleton (lot, optional) |
| Vending machine / ATM | State (machine states), Strategy (change-making), Command (transactions) |
| Splitwise | Strategy (split types), Observer (balance notifications), Factory (expense types) |
| Elevator system | Strategy (scheduling: nearest car, SCAN), State (moving/idle/maintenance), Observer (floor displays) |
| Chess / Snake and Ladder | Factory (pieces), Strategy (move rules per piece), Command (move history and undo) |
| Logger / rate limiter | Singleton, Chain of Responsibility (log levels), Strategy (limiting algorithm), Proxy (rate-limited client) |
| Food delivery / notifications | Observer (order status), Factory and Adapter (channels and vendor SDKs), Decorator (retry/logging wrappers) |
Once these ten are second nature, the next tier worth knowing by name is Chain of Responsibility (middleware, log handlers, approval flows), Template Method (fixed algorithm skeleton with overridable steps), Facade, Composite (file systems, org charts) and Iterator. You will rarely be asked to code them, but recognising them in a follow-up makes you sound fluent. For the end-to-end LLD preparation plan, read our LLD interview guide.
Frequently Asked Questions
Crack Your LLD Round
Prepflix's LLD track walks through parking lots, Splitwise, elevators and more, applying exactly these patterns with mentor code reviews.
Explore the Prepflix Course →