LLD · Design Patterns

Top 10 Design Patterns for Interviews: Code, Use Cases and Traps

The ten Gang of Four patterns that show up again and again in LLD and machine coding rounds, each with compact Java code, a real use case, and the follow-up question interviewers ask next.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 21 min read
Ten interlocking geometric blocks on a blueprint representing software design patterns
TL;DR
  • 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.

💡
The one-sentence ruleBefore introducing any pattern, say out loud what varies. "The algorithm varies" means Strategy. "Behaviour varies with internal state" means State. "Who needs to react to an event varies" means Observer. "Which concrete class to build varies" means Factory.

Design Patterns Cheat Sheet: The Ten at a Glance

PatternTypeWhat varies / problem solvedClassic interview use
SingletonCreationalExactly one shared instanceConfig, logger, parking lot instance
FactoryCreationalWhich concrete class to instantiateVehicle, payment method, notification channel creation
BuilderCreationalMany optional constructor paramsHTTP request, Pizza/Order, immutable DTOs
StrategyBehaviouralInterchangeable algorithmsPricing, fee calculation, split types in Splitwise
ObserverBehaviouralWho reacts to an eventNotifications, stock price alerts, order status
DecoratorStructuralAdd responsibilities at runtimePizza toppings, Java I/O streams, middleware
AdapterStructuralIncompatible interfacesThird-party payment gateway SDKs
StateBehaviouralBehaviour depends on stateVending machine, ATM, order lifecycle
CommandBehaviouralRequests as objects (undo, queue)Text editor undo, remote control, job queue
ProxyStructuralControl access to a real objectCaching, 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.

Java
// 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 add readResolve()), 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.

Java
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.

Java
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.

Java
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.

Java
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 ConcurrentModificationException on an ArrayList. CopyOnWriteArrayList fixes 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.

Java
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.

Java
// 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.

Java
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.

⚠️
Do not over-applyWith two states and two actions, an enum plus a switch is clearer. State pattern earns its keep at roughly three or more states with several actions each. Saying this out loud shows judgment.

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.

Java
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).

Java
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.

PairSame structure?The real difference
Strategy vs StateYes: context holds an interface referenceStrategy is chosen by the client and strategies are independent. States change themselves and know their successors.
Decorator vs ProxyYes: wrapper with same interfaceDecorator adds behaviour and is stacked by the client. Proxy controls access, and usually manages the real object's lifecycle itself.
Adapter vs DecoratorBoth wrapAdapter changes the interface. Decorator keeps the interface and adds behaviour.
Adapter vs FacadeBoth hide somethingAdapter makes one interface match another. Facade gives a simpler front over a whole subsystem (e.g., a CheckoutFacade over inventory, pricing, payment).
Factory vs BuilderBoth createFactory chooses which class in one call. Builder assembles one complex object step by step.
Observer vs Pub/SubSimilar ideaObserver: subject knows its observers directly, usually in-process. Pub/Sub: a broker decouples publishers and subscribers completely, often across services.
Command vs StrategyBoth encapsulate behaviourCommand 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.

  1. 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.
  2. Model entities first, patterns second. Get ParkingLot, Floor, Spot, Ticket right before you think about abstractions. Patterns sit on top of a correct domain model.
  3. 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.
  4. 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.
  5. 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 problemPatterns that naturally fit
Parking lotStrategy (pricing, spot allocation), Factory (vehicles/spots), Observer (display boards), Singleton (lot, optional)
Vending machine / ATMState (machine states), Strategy (change-making), Command (transactions)
SplitwiseStrategy (split types), Observer (balance notifications), Factory (expense types)
Elevator systemStrategy (scheduling: nearest car, SCAN), State (moving/idle/maintenance), Observer (floor displays)
Chess / Snake and LadderFactory (pieces), Strategy (move rules per piece), Command (move history and undo)
Logger / rate limiterSingleton, Chain of Responsibility (log levels), Strategy (limiting algorithm), Proxy (rate-limited client)
Food delivery / notificationsObserver (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.

✅
Practice drillTake the vending machine and rewrite it twice: once with an enum and switch, once with the State pattern. Then add a "maintenance mode". Feeling which version absorbs the change better teaches more than reading any catalogue of patterns.

Frequently Asked Questions

Which design patterns are most asked in interviews?▾
Singleton, Factory, Builder, Strategy, Observer and State come up most often, because they map directly to common LLD problems like parking lots, vending machines, notification systems and payment flows. Decorator, Adapter, Command and Proxy follow closely, usually as follow-up questions.
How many design patterns should I learn for an SDE interview?▾
Ten well-understood patterns beat twenty-three memorised ones. If you can write the ten in this guide from memory, explain when to use each, and name the trade-off, you are covered for nearly every LLD and machine coding round at Indian product companies.
Is Singleton an anti-pattern?▾
It is often overused. A Singleton is global mutable state in disguise, it hides dependencies and makes unit testing harder. It is fine for genuinely single resources like a configuration registry or a connection pool, ideally created once and injected rather than fetched via getInstance everywhere.
What is the difference between Strategy and State pattern?▾
The class diagrams are nearly identical. In Strategy the client chooses the algorithm and strategies do not know about each other. In State the object changes its own behaviour as its internal state changes, and states usually trigger transitions to other states.
What is the difference between Factory Method and Abstract Factory?▾
Factory Method creates one product and lets subclasses or a parameter decide which concrete class to instantiate. Abstract Factory creates a family of related products, such as a dark-theme button plus a dark-theme checkbox, guaranteeing they are used together consistently.
Do I need to name design patterns in a machine coding round?▾
Naming them helps, but only after the design is justified by the requirement. Say what changes and why you isolated it, then mention the pattern name. Forcing patterns where nothing varies is a common red flag.
Are design patterns asked for freshers?▾
Freshers usually get Singleton (including thread-safe versions) and Factory as theory questions, plus OOP fundamentals. Engineers with 2 or more years of experience are expected to apply patterns in an LLD or machine coding round.

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 →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.