Design patterns appear in almost every LLD interview question. Knowing when and how to apply them is what separates candidates who pass from those who don't. This guide covers the 10 most important patterns with real LLD examples from problems asked at Amazon, Flipkart, Uber, and other top companies.
1. Strategy Pattern
Use when: Multiple algorithms or behaviors for the same operation, and you want to swap them at runtime.
LLD examples: Fare calculation in Uber, payment methods in e-commerce, split types in Splitwise, pricing in Parking Lot.
interface FareStrategy { calculate(km: number, min: number): number; }
class EconomyFare implements FareStrategy { ... }
class PremiumFare implements FareStrategy { ... }
// Usage — switch strategy without changing Trip
class Trip { constructor(private fare: FareStrategy) {} }Interview signal: When you see "support multiple X types" or "configurable behavior" — reach for Strategy.
2. Observer Pattern
Use when: One object's state change should notify multiple other objects.
LLD examples: Order status updates in food delivery, trip status in Uber, notification system, social media feed.
interface Observer { update(event: Event): void; }
class Trip {
private observers: Observer[] = [];
notify(event: Event) { this.observers.forEach(o => o.update(event)); }
}
class RiderApp implements Observer { update(e) { showNotification(e); } }
class DriverApp implements Observer { update(e) { refreshUI(e); } }3. Factory / Factory Method Pattern
Use when: Object creation logic is complex or varies based on conditions.
LLD examples: Creating different notification types, vehicle creation in Parking Lot, order creation in food delivery.
class NotificationFactory {
static create(type: "email" | "sms" | "push"): Notification {
switch(type) {
case "email": return new EmailNotification();
case "sms": return new SMSNotification();
case "push": return new PushNotification();
}
}
}4. State Pattern
Use when: An object's behavior changes dramatically based on its internal state.
LLD examples: Driver states in Uber (Available/OnTrip/Offline), ATM states, elevator states, booking states in BookMyShow.
interface DriverState {
acceptTrip(driver: Driver): void;
goOffline(driver: Driver): void;
}
class AvailableState implements DriverState {
acceptTrip(driver: Driver) { driver.setState(new OnTripState()); }
goOffline(driver: Driver) { driver.setState(new OfflineState()); }
}
class OnTripState implements DriverState {
acceptTrip() { throw new Error("Already on trip"); }
goOffline() { throw new Error("Complete trip first"); }
}5. Command Pattern
Use when: You need to encapsulate operations as objects for queuing, logging, or undo.
LLD examples: Job scheduler (each job is a Command), elevator floor requests, inventory operations.
interface Command { execute(): void; }
class ReserveStockCommand implements Command {
constructor(private inventory: Inventory, private productId: string, private qty: number) {}
execute() { this.inventory.reserve(this.productId, this.qty); }
}
// Queue of commands can be replayed, logged, or undone6. Decorator Pattern
Use when: You want to add behavior to an object dynamically without subclassing.
LLD examples: Surge pricing on base fare, adding TTL to cache, layered logging handlers.
class SurgePricingDecorator implements FareStrategy {
constructor(private base: FareStrategy, private multiplier: number) {}
calculate(km: number, min: number): number {
return this.base.calculate(km, min) * this.multiplier;
}
}7. Singleton Pattern
Use when: Exactly one instance should exist globally.
LLD examples: Logger, Config, Connection pool, Cache.
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) Logger.instance = new Logger();
return Logger.instance;
}
}Interview warning: Don't overuse Singleton. Only use it when exactly one instance is genuinely required. Interviewers penalize excessive Singleton usage.
8. Template Method Pattern
Use when: An algorithm has a fixed skeleton but some steps vary by subclass.
LLD examples: Cancellation policy in Hotel booking, report generation, data export formats.
abstract class BookingCancellation {
cancel(booking: Booking): void {
this.validateCancellation(booking); // Fixed step
const refund = this.calculateRefund(booking); // Varies by type
this.processRefund(booking, refund); // Fixed step
}
abstract calculateRefund(booking: Booking): number;
}9. Chain of Responsibility
Use when: A request passes through a series of handlers, each deciding to process or pass it on.
LLD examples: ATM cash dispensing (₹500 → ₹200 → ₹100 notes), fraud detection pipeline, logging handlers, middleware.
abstract class CashHandler {
protected next: CashHandler | null = null;
setNext(h: CashHandler): CashHandler { this.next = h; return h; }
abstract dispense(amount: number): void;
}
class FiveHundredHandler extends CashHandler {
dispense(amount: number) {
const count = Math.floor(amount / 500);
if (count > 0) { console.log(`Dispensing ${count} × ₹500`); }
this.next?.dispense(amount % 500);
}
}10. Composite Pattern
Use when: You need to treat individual objects and groups of objects uniformly.
LLD examples: Chat (1-on-1 and Group Chat share the same interface), file system (File and Folder), UI components.
interface Chat {
sendMessage(msg: Message): void;
getParticipants(): User[];
}
class DirectChat implements Chat { ... } // 1-on-1
class GroupChat implements Chat { ... } // N participants
// TripService.notifyChat(chat: Chat) works for both ✓Pattern Selection Cheatsheet
- Multiple behaviors for one operation → Strategy
- React to state changes across objects → Observer
- Complex object states → State
- Create objects without specifying exact class → Factory
- Add behavior dynamically → Decorator
- Encapsulate operations as objects → Command
- Pass requests through handlers → Chain of Responsibility
- Fixed algorithm, varying steps → Template Method
- One instance globally → Singleton
- Treat individual and group uniformly → Composite