Loading learning content...
Design patterns are one of the most misused tools in software engineering. Candidates often treat them as targets—something to deploy to demonstrate knowledge. But interviewers aren't looking for pattern name-dropping. They're assessing whether you recognize patterns as solutions to specific problems and whether you can apply them judiciously.\n\nThe difference is crucial. A candidate who shoehorns Visitor and Abstract Factory into a simple parking lot design signals inexperience. A candidate who chooses Strategy for pricing because requirements explicitly call for multiple pricing schemes demonstrates disciplined thinking.
By the end of this page, you will understand how interviewers evaluate pattern knowledge, what distinguishes pattern mastery from pattern memorization, which patterns appear most frequently in LLD interviews, and how to demonstrate appropriate application without over-engineering.
When interviewers assess pattern knowledge, they're evaluating a multi-layered competency:\n\nLayer 1: Recognition — Can you identify when a problem fits a pattern's shape? This requires understanding the problem each pattern solves, not just its structure.\n\nLayer 2: Selection — When multiple patterns could apply, can you choose the most appropriate one based on trade-offs and context?\n\nLayer 3: Application — Can you implement the pattern correctly, adapting it to your specific domain rather than force-fitting a textbook example?\n\nLayer 4: Articulation — Can you explain why you chose this pattern, what alternatives you considered, and what trade-offs you're accepting?\n\nLayer 5: Restraint — Do you know when NOT to apply a pattern? Can you resist pattern application when simpler solutions suffice?
Interviewers are immediately skeptical of candidates who use pattern names early and often. Phrases like 'I'll use Factory here and Strategy there and Observer for this...' before understanding the problem deeply suggest pattern-first thinking—a sign of junior reasoning. Patterns should emerge from the problem, not be imposed upon it.
Not all 23 Gang of Four patterns appear equally in LLD interviews. Some are foundational; others rarely arise. Here's a tiered ranking based on interview frequency and applicability:
| Tier | Patterns | Why They Appear Frequently |
|---|---|---|
| Essential (Almost Every Interview) | Factory Method, Strategy, Observer, State, Singleton | These solve fundamental problems: object creation, variable behavior, event handling, status transitions, shared access |
| Common (Many Interviews) | Builder, Decorator, Adapter, Command, Template Method | Complex construction, behavior composition, integration, undo/redo, algorithm skeletons |
| Occasional (Some Interviews) | Facade, Composite, Chain of Responsibility, Memento | Subsystem simplification, hierarchical structures, request handling, state snapshots |
| Rare (Special Contexts) | Flyweight, Bridge, Visitor, Prototype, Abstract Factory | Usually only when specific domain requirements demand them |
Focus your preparation on the Essential tier. Master those fully—understand when they apply, how they work, and their trade-offs. The Common tier should be familiar. The Occasional and Rare tiers require recognition but not deep implementation fluency unless the problem explicitly calls for them.
The Strategy pattern is arguably the most frequently applicable pattern in LLD interviews. Understanding it deeply is essential.\n\nThe Pattern's Purpose:\n\nStrategy defines a family of interchangeable algorithms, encapsulating each one and making them substitutable. It allows selecting the algorithm at runtime without coupling clients to specific implementations.\n\nWhen to Apply Strategy:
LLD Interview Application:\n\nStrategy appears in almost every LLD problem if you look for it:
| System | Strategy Application | Strategies |
|---|---|---|
| Parking Lot | Pricing calculation | HourlyPricing, FlatRatePricing, WeekendPricing, VIPPricing |
| Library | Fine calculation | FlatFineStrategy, ProgressiveFineStrategy, GracePeriodStrategy |
| Elevator | Scheduling algorithm | FCFSScheduler, SCANScheduler, LOOKScheduler |
| Ride Sharing | Fare calculation | StandardFare, SurgeFare, PooledFare |
| Chess | Piece movement | PawnMovement, RookMovement, KnightMovement |
| Payment System | Payment processing | StripeProcessor, PayPalProcessor, BankTransferProcessor |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// The problem: Parking prices vary by time, vehicle, membership, etc.// Without Strategy: massive if/else chains // WITH STRATEGY: Clean, extensible, testable // 1. Define the strategy interfaceinterface PricingStrategy { Money calculate(Vehicle vehicle, Duration duration, ParkingContext context);} // 2. Implement concrete strategiesclass HourlyPricing implements PricingStrategy { private final Map<VehicleType, Money> hourlyRates; public Money calculate(Vehicle vehicle, Duration duration, ParkingContext context) { Money hourlyRate = hourlyRates.get(vehicle.getType()); long hours = Math.max(1, duration.toHours()); // Minimum 1 hour return hourlyRate.multiply(hours); }} class WeekendHourlyPricing implements PricingStrategy { private final PricingStrategy basePricing; private final BigDecimal weekendMultiplier; public WeekendHourlyPricing(PricingStrategy base, BigDecimal multiplier) { this.basePricing = base; this.weekendMultiplier = multiplier; } public Money calculate(Vehicle vehicle, Duration duration, ParkingContext context) { Money basePrice = basePricing.calculate(vehicle, duration, context); if (context.isWeekend()) { return basePrice.multiply(weekendMultiplier); } return basePrice; }} class MemberDiscountPricing implements PricingStrategy { private final PricingStrategy basePricing; private final MembershipService membershipService; public Money calculate(Vehicle vehicle, Duration duration, ParkingContext context) { Money basePrice = basePricing.calculate(vehicle, duration, context); Membership membership = membershipService.getMembership(vehicle.getOwner()); return basePrice.multiply(1 - membership.getDiscountRate()); }} // 3. Context class uses strategyclass ParkingLot { private PricingStrategy pricingStrategy; public void setPricingStrategy(PricingStrategy strategy) { this.pricingStrategy = strategy; } public Ticket checkout(Vehicle vehicle, LocalDateTime entryTime) { Duration parked = Duration.between(entryTime, LocalDateTime.now()); Money price = pricingStrategy.calculate(vehicle, parked, buildContext()); return new Ticket(vehicle, price, parked); }} // 4. Composition allows combining strategies (Decorator/Strategy combo)PricingStrategy pricing = new MemberDiscountPricing( new WeekendHourlyPricing( new HourlyPricing(rates), BigDecimal.valueOf(1.25) ), membershipService);When introducing Strategy, say: 'Pricing varies based on time, vehicle type, membership, and potentially promotions. Rather than a massive conditional, I'm using Strategy so each pricing scheme is encapsulated. This allows adding new schemes without modifying existing code—adhering to Open/Closed Principle.' This connects pattern to problem to principle.
Factory patterns (Factory Method and Abstract Factory) handle object creation in controlled ways. They're essential when:\n\n- Object creation is complex (many parameters, validation)\n- The exact type to create is determined at runtime\n- Creation logic should be centralized\n- Clients shouldn't directly instantiate concrete classes\n\nFactory Method vs Abstract Factory:\n\n- Factory Method: Single method returning objects of a common interface; the method decides the concrete type\n- Abstract Factory: Family of related objects; the factory creates multiple related products
| System | Factory Application | What It Creates |
|---|---|---|
| Parking Lot | VehicleFactory | Different vehicle types from entry data (plate scan) |
| Parking Lot | ParkingSpotFactory | Spot types (compact, regular, large, handicapped) |
| Library | LoanFactory | Different loan types with different rules for different members |
| Chess | PieceFactory | Piece objects from board position notation |
| Notification System | NotificationChannelFactory | Email, SMS, Push notification channel objects |
| Document Editor | DocumentFactory | Word, PDF, Spreadsheet document objects |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// Factory Method for creating parking spots // The interface (product)interface ParkingSpot { boolean canFit(Vehicle vehicle); void occupy(Vehicle vehicle); void vacate(); SpotType getType();} // Concrete productsclass CompactSpot implements ParkingSpot { private static final Set<VehicleType> ALLOWED = Set.of(VehicleType.MOTORCYCLE, VehicleType.COMPACT); public boolean canFit(Vehicle vehicle) { return ALLOWED.contains(vehicle.getType()); } // ... other methods} class LargeSpot implements ParkingSpot { // Can fit any vehicle public boolean canFit(Vehicle vehicle) { return true; } // ... other methods} class ElectricSpot implements ParkingSpot { private ChargingStation chargingStation; public boolean canFit(Vehicle vehicle) { return vehicle.isElectric(); } public void startCharging() { /* ... */ } // ... other methods} // Factory classclass ParkingSpotFactory { public ParkingSpot createSpot(SpotType type, String spotId, Location location) { switch (type) { case COMPACT: return new CompactSpot(spotId, location); case REGULAR: return new RegularSpot(spotId, location); case LARGE: return new LargeSpot(spotId, location); case ELECTRIC: return new ElectricSpot(spotId, location, new ChargingStation()); case HANDICAPPED: return new HandicappedSpot(spotId, location); default: throw new IllegalArgumentException("Unknown spot type: " + type); } } // Factory method with configuration public List<ParkingSpot> createFloor(FloorConfiguration config) { List<ParkingSpot> spots = new ArrayList<>(); for (SpotConfiguration spotConfig : config.getSpots()) { spots.add(createSpot(spotConfig.getType(), spotConfig.getId(), spotConfig.getLocation())); } return spots; }} // UsageParkingSpotFactory factory = new ParkingSpotFactory();ParkingSpot spot = factory.createSpot(SpotType.ELECTRIC, "E-001", location); // Client code doesn't know concrete class, just the interfaceif (spot.canFit(vehicle)) { spot.occupy(vehicle);}If object creation is simple (few parameters, no conditional logic, always the same type), a factory adds unnecessary indirection. 'new User(name, email)' doesn't need a factory. Factories earn their keep when creation logic is complex or type selection is dynamic.
Two patterns frequently appear together in LLD interviews: State for managing complex status transitions, and Observer for reacting to state changes.\n\nState Pattern:\n\nUse State when an object's behavior changes based on its internal state, and when there are many states with complex transition rules. The pattern replaces large switch statements with polymorphic state objects.\n\nObserver Pattern:\n\nUse Observer when multiple components need to react to changes in an object without tight coupling. The subject notifies observers without knowing who they are or what they do.
| System | Stateful Object | States |
|---|---|---|
| Elevator | Elevator | Idle, MovingUp, MovingDown, DoorOpening, DoorClosing, Emergency |
| Order System | Order | Pending, Confirmed, Processing, Shipped, Delivered, Cancelled |
| Vending Machine | Machine | Idle, ProductSelected, MoneyInserted, Dispensing, Refunding |
| ATM | Transaction | CardInserted, PinEntered, AccountSelected, TransactionSelected, Completed |
| Booking | Booking | Draft, Reserved, Confirmed, CheckedIn, Completed, Cancelled |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
// State Pattern for Order with Observer for notifications // State interfaceinterface OrderState { void confirm(Order order); void cancel(Order order, String reason); void ship(Order order, ShippingInfo info); void deliver(Order order); String getStateName();} // Concrete statesclass PendingState implements OrderState { public void confirm(Order order) { // Validate payment, reserve inventory order.setState(new ConfirmedState()); } public void cancel(Order order, String reason) { order.setState(new CancelledState(reason)); } public void ship(Order order, ShippingInfo info) { throw new IllegalStateException("Cannot ship pending order"); } public void deliver(Order order) { throw new IllegalStateException("Cannot deliver pending order"); }} class ConfirmedState implements OrderState { public void confirm(Order order) { throw new IllegalStateException("Already confirmed"); } public void cancel(Order order, String reason) { // Release inventory order.setState(new CancelledState(reason)); } public void ship(Order order, ShippingInfo info) { order.setShippingInfo(info); order.setState(new ShippedState()); } public void deliver(Order order) { throw new IllegalStateException("Must ship before delivering"); }} // Observer interfaceinterface OrderObserver { void onOrderStateChanged(Order order, String oldState, String newState);} // Concrete observersclass CustomerNotificationObserver implements OrderObserver { private final NotificationService notificationService; public void onOrderStateChanged(Order order, String oldState, String newState) { String message = String.format("Your order #%s is now %s", order.getId(), newState); notificationService.notify(order.getCustomer(), message); }} class InventoryObserver implements OrderObserver { private final InventoryService inventoryService; public void onOrderStateChanged(Order order, String oldState, String newState) { if ("Cancelled".equals(newState)) { inventoryService.releaseReservation(order.getItems()); } }} // Order class (Context and Subject)class Order { private OrderState state; private List<OrderObserver> observers = new ArrayList<>(); public Order() { this.state = new PendingState(); } void setState(OrderState newState) { String oldStateName = this.state.getStateName(); this.state = newState; notifyObservers(oldStateName, newState.getStateName()); } public void addObserver(OrderObserver observer) { observers.add(observer); } private void notifyObservers(String oldState, String newState) { for (OrderObserver observer : observers) { observer.onOrderStateChanged(this, oldState, newState); } } // Public methods delegate to state public void confirm() { state.confirm(this); } public void cancel(String reason) { state.cancel(this, reason); } public void ship(ShippingInfo info) { state.ship(this, info); } public void deliver() { state.deliver(this); }}In interviews, justify State by saying: 'The order has multiple states with different allowed transitions. Rather than scattered conditionals checking status everywhere, each state encapsulates its own transition rules. This makes the state machine explicit and testable.' Interviewers appreciate explicit state machines.
Builder Pattern:\n\nUse Builder when object construction is complex, involves many optional parameters, or requires a specific sequence. Builder provides a fluent, readable construction API and ensures only valid objects are created.\n\nCommand Pattern:\n\nUse Command when you need to encapsulate operations as objects, enabling:\n- Undo/redo functionality\n- Operation queuing\n- Operation logging\n- Deferred execution
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// Builder for complex Booking objectpublic class Booking { private final String bookingId; private final User customer; private final Show show; private final List<Seat> seats; private final PaymentMethod paymentMethod; private final BookingStatus status; private final LocalDateTime createdAt; private final Money totalAmount; private final String promoCode; // Optional private final SpecialRequests specialRequests; // Optional private Booking(Builder builder) { // Validation in constructor ensures all Bookings are valid if (builder.seats == null || builder.seats.isEmpty()) { throw new IllegalArgumentException("Booking requires at least one seat"); } if (builder.show.getStartTime().isBefore(LocalDateTime.now())) { throw new IllegalArgumentException("Cannot book past shows"); } this.bookingId = UUID.randomUUID().toString(); this.customer = Objects.requireNonNull(builder.customer); this.show = Objects.requireNonNull(builder.show); this.seats = List.copyOf(builder.seats); // Immutable copy this.paymentMethod = Objects.requireNonNull(builder.paymentMethod); this.status = BookingStatus.PENDING; this.createdAt = LocalDateTime.now(); this.totalAmount = calculateTotal(builder); this.promoCode = builder.promoCode; this.specialRequests = builder.specialRequests; } public static class Builder { private User customer; private Show show; private List<Seat> seats = new ArrayList<>(); private PaymentMethod paymentMethod; private String promoCode; private SpecialRequests specialRequests; public Builder customer(User customer) { this.customer = customer; return this; } public Builder show(Show show) { this.show = show; return this; } public Builder addSeat(Seat seat) { this.seats.add(seat); return this; } public Builder seats(List<Seat> seats) { this.seats = new ArrayList<>(seats); return this; } public Builder payWith(PaymentMethod method) { this.paymentMethod = method; return this; } public Builder promoCode(String code) { this.promoCode = code; return this; } public Builder specialRequests(SpecialRequests requests) { this.specialRequests = requests; return this; } public Booking build() { return new Booking(this); } } public static Builder builder() { return new Builder(); }} // Usage - clear, readable, validatedBooking booking = Booking.builder() .customer(currentUser) .show(selectedShow) .seats(selectedSeats) .payWith(new CreditCard("...")) .promoCode("SAVE20") .build();In game or editor LLD problems (chess, drawing app, text editor), Command pattern often appears for undo/redo. Each action becomes a command object with execute() and undo() methods. A command stack enables undo; a redo stack enables redo. This is a classic, expected pattern for such problems.
Demonstrating restraint is as important as demonstrating pattern knowledge. Interviewers respect candidates who recognize when patterns add complexity without sufficient benefit.\n\nSigns a pattern is overkill:
| Scenario | Pattern Approach | Simple Approach | Better Choice |
|---|---|---|---|
| Only email notifications, no plans for others | NotificationStrategy with EmailNotificationStrategy | EmailNotificationService directly | Simple — abstract when new channels are actually needed |
| Boolean flag for active/inactive account | State pattern with ActiveState, InactiveState | boolean isActive field with simple conditionals | Simple — two states with simple behavior don't need pattern |
| User object with 3 required fields | User.Builder pattern | Constructor with 3 parameters | Simple — no optional parameters, no complexity |
| 10 pricing variations, new ones added monthly | Direct if/else chain | PricingStrategy pattern | Pattern — high variability + frequent changes justifies structure |
You Aren't Gonna Need It. Don't add patterns for hypothetical future needs. Add them when current requirements demand them or when requirements explicitly indicate future variation. Interviewers who hear 'I'm adding this interface in case we need...' without evidence are skeptical.
How you present pattern usage matters as much as the patterns themselves. Here's how to demonstrate mastery:
Before applying a pattern, ask: 'What specific problem am I solving?' If you can't articulate the problem clearly, you probably don't need the pattern. This self-check prevents pattern overuse and sharpens your thinking for articulating choices to interviewers.
We've explored the third dimension of interview evaluation: pattern knowledge and its appropriate application. Let's consolidate the key insights:
What's next:\n\nWith design thinking, OOP understanding, and pattern knowledge covered, the final piece is communication skills—how you present your design, explain your reasoning, handle questions, and collaborate with the interviewer to refine the solution.
You now understand how to demonstrate pattern knowledge in LLD interviews. The goal isn't to deploy as many patterns as possible—it's to select the right patterns for actual problems and articulate why. Next, we'll master the communication dimension that brings all these skills together.