Object-oriented / low-level design
📖 Walk me through it — plain English
This lesson is about a specific kind of interview question: "Design a parking lot," "Design an elevator," "Design Splitwise." The interviewer isn't asking for a clever algorithm. They want to see whether you can take a messy real-world thing and carve it into classes (blueprints for objects, like a Car blueprint that knows its size) that fit together cleanly. "Object-oriented design" (OOD) just means organizing your code around things (objects) that hold their own data and their own behavior, instead of one giant pile of functions. "Low-level design" (LLD) is the same exercise under another name: it sits one level below system design (which talks about servers, databases, and queues) and instead designs the classes and methods inside a single service.
Think of it like designing the org chart for a small company before anyone is hired. You don't write anyone's daily to-do list yet. You decide the roles first: who is the receptionist, who is accounting, who manages the floor — and you make sure each role has one clear job and knows who to hand work off to. If the receptionist also did payroll and cleaned the bathrooms, every change to any one of those jobs would risk breaking the other two. OOD is drawing that clean org chart for your code.
The lesson gives you three toolkits. SOLID is five health checks for your design — the most important being "Single responsibility" (each class has exactly one reason to change, like each role having one job). Design patterns (Factory, Strategy, Observer, etc.) are named, reusable shapes that keep showing up — you should recognize them, not memorize them. For example, "Strategy" means you can swap out one piece of logic at runtime, like changing the parking price rule for weekdays vs. event nights without touching the rest of the code.
Because this is a recipe, not a puzzle, here is the loop the lesson hands you — follow it in order every time:
The worked parking-lot sketch is exactly this loop applied. Notice how the jobs are split: Spot only tracks one single space, ParkingLot only decides which spot to assign, and Ticket only computes the fee. That's "single responsibility" in action — and if pricing ever needs to differ (weekday vs. event vs. EV), you slot in a separate PricingStrategy object instead of tangling that logic into Ticket.
Why this approach wins the interview: a clean split means each future change touches one class, not five. That's the whole point — the interviewer is checking whether your design will survive the next feature request without collapsing. Talk out loud as you walk the six steps; the structured thinking is what they're grading, far more than perfect syntax.
"Design a parking lot / elevator / Splitwise / library." Distinct round at many companies. Structured the same way every time.
The vocabulary, defined once
Before the strategy, the words. OOD has a small dialect, and interviewers expect you to use it precisely. Every term below is defined inline so you never have to leave this page. Read it once slowly; the rest of the lesson reuses these words constantly.
- Class — a blueprint. It declares what data an object will hold (its fields/attributes) and what it can do (its methods).
Carthe class is not any particular car; it's the template. - Object (instance) — one concrete thing built from a class.
my_car = Car("blue")makes one object. A class is the cookie cutter; objects are the cookies. - Field / attribute / state — the data an object carries (a car's
color, a spot'sis_free). "State" is just the current values of those fields. - Method — a function that belongs to a class and usually acts on the object's own state:
spot.park(car). - Encapsulation — bundling data with the methods that touch it, and hiding the internals behind a small public surface. Outsiders call
spot.park(v); they never reach in and flipspot.vehicledirectly. This is what lets you change the inside without breaking callers. - Abstraction — exposing only what matters and hiding the how.
pay(amount)is the abstraction; whether it's cash, card, or wallet is hidden behind it. Good abstractions read like the problem domain, not like plumbing. - Inheritance ("is-a") — a child class reuses and specializes a parent.
Car extends Vehiclemeans a Car is a Vehicle and gets its fields/methods for free, overriding the ones that differ. - Polymorphism ("many shapes") — code written against a parent type works on any child. A loop over
Vehicle[]can call.sizeon each, and Cars and Trucks each answer correctly. You write the loop once; new vehicle types just slot in. - Composition ("has-a") — building a class by holding other objects as fields.
ParkingLothas a list ofSpots. Most relationships are has-a, not is-a. - Interface — a contract of method signatures with no implementation: "anything that claims to be a
PricingStrategymust offerfee(duration)." Lets unrelated classes be used interchangeably. - Abstract class — a partial blueprint: some methods implemented, at least one left abstract (unfinished) so it can't be instantiated directly and must be subclassed.
Vehicleis abstract — you never park a generic "Vehicle," only a Car or Truck.
Composition over inheritance — the single most useful heuristic in this lesson. Inheritance is rigid: a subclass is welded to its parent forever, and a deep tree (A → B → C → D) becomes impossible to change because everyone depends on everyone above them. Composition is flexible: instead of saying "an EVCar is a Car that is a chargeable thing," you give a plain Car a charger field (a Charger object) when it needs one. Prefer "has-a" unless the "is-a" relationship is genuinely permanent and total. When in doubt, compose.
The four OOD steps, defined
Every LLD prompt collapses into the same four moves. The six-step loop above is the interview-day expansion of these; this is the skeleton underneath it. Memorize this order — it is the thing you fall back on when the prompt is unfamiliar.
Turn the vague prompt into a written list of features and a list of non-goals. "Track which spots are free, issue tickets, charge by the hour" is in; "online reservations" is out (for now). Say the non-goals out loud — scoping is graded.
Pull the nouns from your requirements; each becomes a candidate class. ParkingLot, Spot, Vehicle, Ticket, PricingStrategy. Give each one its single responsibility in a sentence before you write any code.
Wire the entities: which has-a which (composition), which is-a which (inheritance), which depends on an interface. Draw boxes and lines — this is the moment a god class shows up as a box with too many arrows.
Pull the verbs into methods and write their signatures: park(v) → Ticket, leave(ticket) → Fee. A signature is the method name plus its inputs and output type. Signatures are the contract; the body can come later.
UML-lite: how to draw it
UML (Unified Modeling Language) is a standard for diagramming designs. You do not need the full spec — interviewers want a "UML-lite" class box, drawn on a whiteboard in seconds. Each class is a rectangle split into three: the name at top, the fields below (prefix - for private, + for public), and the methods at the bottom. Then connect them: a plain line for "has-a" (composition), and a hollow arrow pointing at the parent for "is-a" (inheritance). That is the entire notation you need, and it's exactly what the parking-lot sketch below uses.
SOLID
SOLID is five design heuristics, one per letter. Treat them as smells-detectors, not laws: when a design feels brittle, one of these is usually being violated. Each one-liner below is paired with the parking-lot example so it isn't abstract.
- S — Single responsibility: a class has one reason to change.
Ticketcomputes fees; it does not also print receipts and email the customer — that's three reasons to change crammed into one box. - O — Open/closed: open for extension, closed for modification. Add subclasses, don't edit working ones. New
EVSpot? SubclassSpot— leave the testedSpotalone. - L — Liskov substitution: subclasses must behave like the parent (no surprise exceptions). Anywhere code expects a
Spot, anEVSpotmust drop in without breaking it. - I — Interface segregation: many small interfaces beat one fat one. No one should depend on methods they don't use. A read-only viewer of the lot shouldn't be handed
park()andleave(). - D — Dependency inversion: depend on abstractions, not concretions. Inject collaborators.
ParkingLottakes aPricingStrategyinterface, not a hard-codedWeekdayPricing— so you can swap pricing without touching the lot.
Patterns to recognize
A design pattern is a named, reusable arrangement of classes that solves a recurring problem. You are not expected to recite them — you are expected to recognize when one fits and name it ("this is just Strategy") so the interviewer knows you've seen the shape before. These are the ones that actually earn their keep in an LLD round.
- Factory: separate construction from use — a method that returns the right subclass based on input, so callers don't litter
new Car()/new Truck()everywhere. Helpful when subclass choice depends on input. - Strategy: swap an interchangeable algorithm at runtime by passing in an object that implements a shared interface (e.g., a pricing strategy). The classic answer to "what if the rule changes?"
- Observer: pub-sub for in-process events — objects "subscribe" and get notified when something changes, instead of the source polling them. A spot-freed event waking up a waitlist.
- Singleton: exactly one instance, globally reachable. Often abused — usually a global in disguise, and it makes testing hard. Reach for it rarely.
- Decorator: wrap an object in another that adds behavior, without subclassing — stack features by stacking wrappers (a
Spotwrapped to add reservation logic). - State: object behavior changes with internal state, each state its own class (e.g., elevator: idle / moving / doors-open), instead of a tangle of
if status == ....
The OOD interview loop
- Clarify — what features? What's out of scope?
- Identify entities — nouns: ParkingLot, Spot, Vehicle, Ticket.
- Relationships — has-a, is-a. Draw boxes and lines.
- Behaviors — verbs as methods: park(), leave(), calculate_fee().
- Edge cases — full lot, invalid ticket, vehicle too tall.
- Extensibility — new spot types? Multi-floor? Reservation?
This loop generalizes to any LLD prompt — elevator, library, vending machine, chess. Swap the nouns and verbs; the skeleton is identical. The discipline that wins: narrate each step ("the nouns I'm hearing are..."), draw as you go, and never jump to code before relationships are on the board. An interviewer who sees the loop knows you'll handle a prompt they've never asked.
Worked example — Parking Lot
Single-responsibility: Spot tracks one space, ParkingLot orchestrates assignment, Ticket handles fee computation. Add a PricingStrategy interface if pricing differs between weekday/event/EV charging.
Let's walk the diagram class by class — this is the level of detail you'd talk through on the whiteboard. For each class, state its one responsibility, then its key fields and method signatures (name → inputs → output).
Vehicle(abstract) — responsibility: describe a thing that can be parked. Field:size: SMALL | MED | LRG. No methods of its own; it exists so the rest of the system can talk to "any vehicle" polymorphically.CarandTruckextend it (is-a) and set their own size.Spot— responsibility: track the state of exactly one physical space. Fields:size,vehicle(null when empty). Methods:park(v: Vehicle) → bool(place a vehicle if it fits and the spot is free),free() → void(clear it). It knows nothing about pricing or the whole lot — just itself.ParkingLot— responsibility: orchestrate assignment across many spots. Field:spots: Spot[](has-a, composition). Methods:park(v: Vehicle) → Ticket(find a free, fitting spot, occupy it, issue a ticket),leave(t: Ticket) → Fee(free the spot, compute the charge). This is the only class that sees the big picture.Ticket— responsibility: record one parking session and compute its fee. Fields:id,spot,entry_ts(entry timestamp). Method:fee(rate) → Money(charge based on elapsed time). Hand off the actual rate logic to aPricingStrategythe moment pricing gets more than one rule.PricingStrategy(interface, add when needed) — responsibility: encapsulate one pricing rule behindfee(duration) → Money.WeekdayPricing,EventPricing,EVPricingeach implement it.Ticket(orParkingLot) holds one via dependency injection — Strategy pattern, and Dependency Inversion, in one move.
Notice what this split buys you. "Add EV charging" → subclass Spot into EVSpot with a charge() method; ParkingLot is untouched (open/closed). "Pricing differs on event nights" → add an EventPricing strategy; no existing class changes. "Multi-floor" → ParkingLot holds a list of Floors, each holding spots. Every feature lands in one place because each class owns one job. That is the whole payoff of doing the four steps in order.
The process for any LLD prompt
Take the parking-lot walkthrough and strip out the parking-specific words; what's left is a repeatable script you can run on an elevator, a vending machine, a chess board, or Splitwise:
What interviewers look for — and the pitfalls
The score sheet is mostly about thinking, not syntax. Here's what earns points and the two traps that lose them.
- You scope before you design (clarifying questions first).
- Clean single-responsibility classes with sensible has-a / is-a wiring.
- Method signatures that read like the problem domain.
- You handle edge cases unprompted and reason about extensibility.
- You think out loud and revise when they push back.
- God class — one class (often "Manager" or "System") that holds all the data and does everything. It violates single-responsibility and is the #1 red flag. Split it.
- Premature inheritance — building deep is-a trees for things that should compose. Forces edits up the chain and breaks Liskov. Prefer has-a.
- Pattern soup — bolting on Factory/Singleton/Observer to look clever. Use a pattern only when it removes real pain.
- Coding too early — writing method bodies before relationships are settled. You'll redesign on every clarification.
Takeaway: OOD/LLD is org-chart design for code. Run the four steps — requirements → entities → relationships → APIs — every time, give each class one responsibility, prefer composition over inheritance, and let SOLID flag the smells. Patterns are shapes to recognize, not trophies to collect. Name your edge cases and one clean extension, narrate the whole way, and avoid the god class. The structure is what's graded — not the syntax.
Go deeper (optional): the canonical pattern catalog is the "Gang of Four" book (Design Patterns, 1994); Refactoring Guru is a friendlier free reference. For the principles, Robert Martin's writing on SOLID is the source. None are required — everything you need for the round is on this page.