Week 12: Low-Level Design: OOP & Design Patterns

Weeks 1–11 covered high-level design — the boxes-and-arrows view of how a whole system fits together. Many interview loops also include a low-level design (LLD) round, which zooms all the way in: given a prompt like "design a parking lot," you're evaluated on the classes you define, the interfaces between them, and whether the design can absorb a new requirement without a rewrite. This week covers the SOLID principles and the handful of design patterns that actually earn their complexity in interviews, then applies both to a full worked class design — the same reasoning Week 13's case study assumes you already have when it turns to an end-to-end system.

Module 9 of 24 Week 12 of 28 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Apply the five SOLID principles to critique and improve a class design
  • Recognize when Strategy, Observer, Factory or Singleton is the right pattern for a design prompt
  • Produce a complete, extensible class design for a real interview-style LLD prompt

1. SOLID Principles in an LLD Interview

SOLID is five principles for keeping a class design maintainable as requirements change — which is exactly what an LLD interview tests, since the interviewer will almost always add a requirement partway through ("now support monthly parking passes") and watch how much of your design survives it.

SOLID — one line each, with a parking-lot example
S — Single Responsibility: a class has one reason to change.
    ParkingLot manages spots and tickets; it should NOT also
    calculate payment amounts or send email receipts.

O — Open/Closed: open for extension, closed for modification.
    Adding a new pricing scheme shouldn't require editing the
    existing pricing code -- it should mean adding a new class.

L — Liskov Substitution: a subtype must be usable anywhere its
    base type is expected, without breaking behavior.
    A Motorcycle subclass of Vehicle must not violate assumptions
    the base Vehicle contract makes (e.g. "always occupies exactly
    one spot").

I — Interface Segregation: don't force a class to implement
    methods it doesn't need. A narrow PayableEntity interface
    beats one giant Vehicle interface with fifteen methods.

D — Dependency Inversion: depend on abstractions, not concrete
    classes. ParkingLot should depend on a PricingStrategy
    interface, never directly on an HourlyPricingStrategy class.

Dependency Inversion is the one most candidates under-apply. A common mistake is writing a ParkingLot class whose fee calculation directly instantiates new HourlyPricingStrategy() — that single line quietly violates Open/Closed too, because now every future pricing scheme requires editing ParkingLot itself rather than adding a new class beside it. The fix is the same move Section 2 formalizes as the Strategy pattern: inject a PricingStrategy interface into ParkingLot's constructor, and let the caller decide which concrete implementation to use.

Name the principle when you apply it, don't recite all five upfront

Opening an LLD interview with a recital of all five SOLID letters signals memorization, not judgment. A stronger approach: design naturally, and when you make a decision that happens to be Open/Closed or Dependency Inversion in action, say so in one sentence — "I'm injecting the pricing strategy rather than hardcoding it, so adding a new pricing tier later won't touch this class." That's the same principle, demonstrated instead of recited.

2. Four Patterns You'll Actually Use

Dozens of design patterns exist; a small handful cover the overwhelming majority of what shows up in LLD interviews. Reaching for the right one at the right moment matters far more than knowing all of them by name.

four patterns, one real use case each
Strategy — interchangeable algorithms behind one interface.
  Use case: PricingStrategy (hourly, flat-rate, membership) for
  the parking lot; the ParkingLot class calls strategy.calculateFee(...)
  without knowing which concrete strategy it's using.

Observer — objects subscribe to be notified when state changes.
  Use case: a DisplayBoard at each level subscribes to spot
  occupancy changes, so it updates "42 spots available" without
  ParkingLot needing to know DisplayBoard exists.

Factory — encapsulate object-creation logic behind one method.
  Use case: VehicleFactory.create(type) returns a Car, Motorcycle
  or Bus instance from a raw type string at the entry gate, so
  callers never write vehicle-type conditionals themselves.

Singleton — guarantee exactly one instance exists.
  Use case: the ParkingLot instance itself, since a physical lot
  has exactly one authoritative record of spot availability.
  Caution: singletons introduce global mutable state and make
  unit testing harder -- justify one before reaching for it.

Strategy and Observer tend to make a design more extensible without adding real complexity, which is why interviewers like seeing them appear naturally. Singleton is the pattern worth being the most careful with: it's genuinely correct for something like a single physical parking lot, but candidates who reach for Singleton on everything ("make Vehicle a singleton too") are usually solving a problem that doesn't exist, at the cost of a design that's harder to test and harder to reason about under concurrency.

A pattern is a means, not a goal

Forcing a Factory onto a class with one constructor call and no variation is a worse answer than a plain constructor — it adds indirection without solving anything. Introduce a pattern only when it removes a real piece of duplication or coupling in front of you; if asked "why Factory here?" you should be able to name the specific conditional logic it replaced.

3. Worked Example: A Parking Lot System

Requirements first, the same discipline Week 1 established: a lot has multiple levels; each level has spots of different sizes (motorcycle, compact, large); a vehicle enters through a gate and receives a ticket; a vehicle exits by paying based on duration and vehicle type; the system tracks real-time availability per spot type.

parking lot — core classes
class ParkingLot {              // Singleton -- one physical lot
  levels: List<Level>
  pricingStrategy: PricingStrategy   // Dependency Inversion (Strategy)
  observers: List<DisplayBoard>      // Observer

  parkVehicle(vehicle: Vehicle): Ticket
  unparkVehicle(ticket: Ticket): Receipt
  notifyObservers(): void
}

class Level {
  spots: List<ParkingSpot>
  findAvailableSpot(vehicleType: VehicleType): ParkingSpot?
}

abstract class ParkingSpot {
  id: string
  isOccupied: boolean
  spotType: SpotType              // MOTORCYCLE, COMPACT, LARGE
  canFit(vehicle: Vehicle): boolean
}

abstract class Vehicle {
  licensePlate: string
  vehicleType: VehicleType
}
class Car extends Vehicle {}
class Motorcycle extends Vehicle {}
class Bus extends Vehicle {}        // needs multiple LARGE spots

class Ticket {
  vehicle: Vehicle
  spot: ParkingSpot
  entryTime: Timestamp
}

interface PricingStrategy {         // Strategy
  calculateFee(ticket: Ticket, exitTime: Timestamp): Money
}
class HourlyPricingStrategy implements PricingStrategy { ... }
class FlatRatePricingStrategy implements PricingStrategy { ... }

class VehicleFactory {              // Factory
  static create(type: string, plate: string): Vehicle
}

Notice how the earlier sections show up concretely here, not as abstract vocabulary: PricingStrategy is Strategy plus Dependency Inversion together — ParkingLot never knows which concrete pricing class it's using, so adding a MembershipPricingStrategy later touches zero existing code, which is exactly what Open/Closed asks for. DisplayBoard as an Observer means ParkingLot can support any number of subscribers — a mobile app's live availability feed, a physical sign at the entrance — without ParkingLot's own code changing when a new subscriber type is added.

One design decision worth stating out loud in an interview: what happens when two vehicles arrive at the same open spot simultaneously? Level.findAvailableSpot and the act of marking a spot occupied need to happen atomically — the same check-then-act race condition Week 11 solved with an atomic Redis script shows up here at the level of a single process, and needs a lock (or an atomic compare-and-swap on the spot's occupied flag) around the find-and-claim step, not two separate operations a second thread could interleave with.

Draw the class diagram before writing method bodies

In a 45-minute LLD round, the classes, their relationships and their public method signatures are what get evaluated — full method implementations rarely fit in the time available and aren't usually what's being scored. Spend the bulk of your time on the shape in the code block above; write a method body only if the interviewer specifically asks to see one worked through.

4. Hands-on Exercise

Hands-on

Design an elevator system

A building has multiple elevators serving multiple floors. Riders press an up/down button on a floor, and a floor button inside the elevator; some elevator has to be dispatched to serve each request.

Requirements:

  1. List the core classes for this system (at minimum, something representing an individual elevator, a request, and a component that decides which elevator handles which request).
  2. Identify at least two SOLID principles that meaningfully shape this design, and state exactly which class or decision each one applies to.
  3. Name the design pattern you'd use for the dispatch logic that picks which elevator serves an incoming request, and justify why that pattern fits better than a hardcoded if/else chain.
  4. Write the method signature (not the full implementation) for the dispatch decision, including what inputs it needs to make a good choice.
  5. Describe one concurrency edge case — two floor requests arriving in the same instant — and state, in 2–3 sentences, how your design avoids assigning the wrong elevator or losing a request.
Hint

Think about what varies here: the dispatch algorithm could be "nearest idle elevator," "least busy elevator," or something smarter — exactly the kind of interchangeable behavior Section 2's Strategy pattern exists for. Design the interface so a smarter dispatch algorithm could be swapped in later without touching the Elevator class itself.

5. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does the Dependency Inversion Principle matter specifically when introducing the Strategy pattern?

Strategy only delivers its benefit — swapping algorithms without touching the calling class — if the calling class depends on the strategy interface rather than a concrete implementation. A ParkingLot that still directly instantiates HourlyPricingStrategy internally has the Strategy classes sitting nearby but hasn't actually achieved Open/Closed; Dependency Inversion (injecting the interface, not the concrete class) is what makes the swap possible.

Q2

What's the risk of reaching for Singleton by default on every class in a design, rather than justifying it case by case?

Singleton introduces global mutable state, which makes unit testing harder (tests can't easily create an isolated instance) and makes concurrent access harder to reason about correctly. It's justified for something with a genuine real-world single-instance constraint, like one physical parking lot, but applying it to classes like Vehicle or Ticket — which legitimately need many instances — is a misuse the interviewer is likely testing for.

Q3

When does introducing a Factory actually earn its complexity, versus just calling a constructor directly?

A Factory earns its place when object creation involves real branching logic the caller would otherwise have to duplicate — deciding which of several concrete subclasses to instantiate based on some input, like VehicleFactory.create(type) picking between Car, Motorcycle and Bus. If there's exactly one concrete class and no variation in how it's constructed, a Factory just adds a layer of indirection over what a plain constructor already does.

Q4

Why can't "find an available spot" and "mark it occupied" be two separate steps in a multi-threaded parking lot system?

If those two steps aren't atomic, two vehicles arriving at nearly the same instant can both find the same open spot before either has marked it occupied, and both get assigned to it — a check-then-act race condition, the same category of bug Week 11 solved with an atomic Redis script for distributed rate-limit counters. The fix here is a lock or an atomic compare-and-swap around the combined find-and-claim operation so no second thread can interleave in the middle.