Episode 9 — System Design / 9.1 — LLD Foundations
Interview Questions: LLD Foundations
Practice questions with model answers for Low-Level Design fundamentals commonly asked in software engineering interviews.
How to use this material (instructions)
- Read lessons in order —
README.md, then9.1.a→9.1.e. - Practice out loud — 60–120 seconds per question before reading the model answer.
- Draw — class diagrams and sequence diagrams reward visual thinking.
- Pair with exercises —
9.1-Exercise-Questions.md. - Quick review —
9.1-Quick-Revision.mdthe night before.
Beginner (Q1–Q4)
Q1. What is Low-Level Design, and how does it differ from High-Level Design?
Why interviewers ask: This tests whether you understand the spectrum of system design and can operate at the right level of abstraction depending on the question.
Model answer:
Low-Level Design (LLD) is the process of translating high-level architecture into code-level structure — deciding which classes exist, what attributes and methods they have, and how they relate to each other. It sits between architecture diagrams and actual implementation code.
| Dimension | HLD | LLD |
|---|---|---|
| Focus | System-wide (services, databases, protocols) | Module-level (classes, interfaces, patterns) |
| Artifacts | Architecture diagrams, data flow | Class diagrams, sequence diagrams |
| Key question | "What services do we need?" | "What classes do we need inside this service?" |
| Scale | Distributed systems | Single-process code organization |
Analogy: HLD is the city plan (where buildings go, how roads connect). LLD is the floor plan of a single building (where rooms go, how doors connect them).
What to emphasize: LLD is about extensibility, maintainability, and clear responsibilities — not just making the code work.
Q2. What are the four pillars of OOP, and why do they matter for LLD?
Why interviewers ask: OOP is the foundation of most LLD work. This checks whether you understand the pillars beyond textbook definitions.
Model answer:
The four pillars are:
-
Encapsulation — Bundling data and methods together and restricting direct access to internal state. In LLD, this prevents invalid states and localizes changes. If the balance calculation logic changes, only the
Accountclass needs updating — not every file that touches balance. -
Inheritance — A child class inherits the parent's properties and methods, enabling code reuse for "is-a" relationships. In LLD, it lets us model hierarchies like
Vehicle → Car, Truck, Motorcyclewithout duplicating code. But it should be used sparingly — deep hierarchies become brittle. -
Polymorphism — Different objects respond to the same method call in their own way. This is the most powerful pillar for LLD because it enables writing code that depends on abstractions (
PaymentProcessor.charge()) rather than specific implementations (StripeProcessor.charge()). New implementations can be added without modifying existing code. -
Abstraction — Exposing only essential details and hiding complexity. In LLD, this means the
Databaseclass exposesquery()andsave()— callers never need to know about connection pooling, retries, or query building.
Why they matter together: They enable writing code that is modular (encapsulation), reusable (inheritance), flexible (polymorphism), and simple to use (abstraction).
Q3. What is the difference between composition and aggregation? Give an example of each.
Why interviewers ask: This is the most commonly confused relationship in LLD. Getting it right demonstrates genuine understanding of object lifecycle management.
Model answer:
Both are "has-a" relationships, but they differ in ownership strength and lifecycle coupling:
Aggregation (weak "has-a"):
- The part can exist independently of the whole.
- If the whole is destroyed, the parts survive.
- Parts can belong to multiple wholes.
- Example:
Department ◇── Employee. If the department is dissolved, employees still exist and can join other departments.
Composition (strong "has-a"):
- The part cannot exist without the whole.
- If the whole is destroyed, the parts are destroyed too.
- Parts belong to exactly one whole.
- Example:
House ◆── Room. If the house is demolished, the rooms cease to exist. You cannot have a floating room without a house.
The litmus test: Ask two questions:
- Can the part exist without the whole? (Yes → Aggregation, No → Composition)
- Can the part belong to multiple wholes? (Yes → Aggregation, No → Composition)
Code signal: In aggregation, parts are typically passed in from outside. In composition, parts are typically created internally by the whole.
// Aggregation: Playlist receives songs from outside
class Playlist {
addSong(song: Song): void { this.songs.push(song); }
}
// Composition: Order creates line items internally
class Order {
addItem(productId: string, qty: number, price: number): void {
this.items.push(new OrderLineItem(productId, qty, price));
}
}
Q4. Why should we "favor composition over inheritance"? When is inheritance still appropriate?
Why interviewers ask: This tests design maturity. Junior devs overuse inheritance; senior devs know when each is appropriate.
Model answer:
Why composition is generally preferred:
-
Flexibility at runtime — Composition allows swapping behaviors at runtime. If a
Characteruses composition (has-a Weapon), you can change the weapon mid-game. With inheritance (SwordCharacter extends Character), you cannot change the character's weapon type. -
Avoids the fragile base class problem — Changes to a parent class can break all child classes in unexpected ways. Composition isolates changes.
-
No diamond problem — Multiple inheritance creates ambiguity. Composition with interfaces avoids this entirely.
-
Better testability — Composed dependencies can be mocked easily. Inherited behavior is harder to isolate in tests.
When inheritance IS appropriate:
- Clear, stable "is-a" relationship:
Circle IS-A Shape,Dog IS-A Animal. - The hierarchy is shallow (2–3 levels maximum).
- The parent provides significant shared behavior that all children need.
- The relationship is unlikely to change over the system's lifetime.
Example of the problem:
// Inheritance creates rigidity:
class Bird extends Animal { fly() { ... } }
class Penguin extends Bird { fly() { throw "Can't fly!" } }
// Penguin is forced to override fly() with an exception — bad design.
// Composition is flexible:
interface FlyBehavior { fly(): void; }
class CanFly implements FlyBehavior { fly() { console.log("flying"); } }
class CannotFly implements FlyBehavior { fly() { console.log("grounded"); } }
class Bird {
constructor(private flyBehavior: FlyBehavior) {}
performFly() { this.flyBehavior.fly(); }
}
// Each bird gets the right behavior without forced overrides.
Intermediate (Q5–Q8)
Q5. Walk me through your approach to solving an LLD interview question.
Why interviewers ask: They want to see if you have a structured methodology or if you jump straight into code.
Model answer:
I follow a 6-step framework:
Step 1: Clarify Requirements (5 minutes) I ask questions to define scope: What features are in scope? Who are the actors? Are there constraints (concurrency, scale)? What is explicitly out of scope?
Step 2: Identify Entities (5 minutes) I extract nouns from the requirements — these become candidate classes. I also identify verbs (methods) and adjectives (enums). I filter out duplicates and group related entities.
Step 3: Define Classes (10 minutes) For each entity, I define attributes (with types and visibility) and state the class's single responsibility. I draw class boxes on the board.
Step 4: Establish Relationships (8 minutes) I connect classes with the correct relationship types — composition for strong ownership, aggregation for weak, association for references, inheritance for "is-a", interfaces for "can-do". I add multiplicity (1, , 1..) to every line.
Step 5: Define Methods (10 minutes)
I write the key method signatures and implement the core logic — typically the 2–3 most important operations (e.g., enterVehicle(), exitVehicle() for a parking lot).
Step 6: Handle Edge Cases (7 minutes) I proactively raise edge cases: What if the lot is full? What about concurrency? Payment failure? I also mention design patterns I would apply (Strategy for pricing, Observer for notifications, Factory for object creation).
Why this works: It gives the interviewer a clear structure to follow, prevents me from getting lost, and ensures I cover all the dimensions they are evaluating.
Q6. Explain the difference between an abstract class and an interface with a practical example.
Why interviewers ask: Many devs confuse the two or use them interchangeably. This tests nuanced understanding.
Model answer:
| Feature | Abstract Class | Interface |
|---|---|---|
| Implementation | Can have concrete methods with logic | Only method signatures (no implementation) |
| State | Can have properties with default values | No state (in TypeScript interfaces at the type level) |
| Constructor | Yes | No |
| Multiple inheritance | A class can extend only ONE abstract class | A class can implement MANY interfaces |
| When to use | Shared behavior + enforced contract | Pure contract for polymorphism |
Practical example — Notification System:
// INTERFACE: defines what any notifier must do
interface Notifier {
send(to: string, message: string): Promise<boolean>;
}
// ABSTRACT CLASS: shared behavior + enforced contract
abstract class BaseNotifier implements Notifier {
protected retryCount: number = 3; // shared state
// Shared implementation — all notifiers retry the same way
async send(to: string, message: string): Promise<boolean> {
for (let i = 0; i < this.retryCount; i++) {
const success = await this.doSend(to, message);
if (success) return true;
}
return false;
}
// Abstract — each notifier sends differently
protected abstract doSend(to: string, message: string): Promise<boolean>;
}
class EmailNotifier extends BaseNotifier {
protected async doSend(to: string, message: string): Promise<boolean> {
// SMTP sending logic
return true;
}
}
class SMSNotifier extends BaseNotifier {
protected async doSend(to: string, message: string): Promise<boolean> {
// Twilio API logic
return true;
}
}
The key insight: Use an interface when you want to define a contract that multiple unrelated classes can fulfill. Use an abstract class when you want to share common behavior among a family of related classes.
Q7. What is a UML class diagram, and how would you draw one in an interview?
Why interviewers ask: Class diagrams are the primary visual artifact in LLD interviews. They want to see if you can communicate your design visually.
Model answer:
A UML class diagram shows the static structure of a system — classes, their attributes, methods, and relationships. Each class is a box with three compartments:
┌─────────────────────────┐
│ ClassName │ ← Name (bold/centered)
├─────────────────────────┤
│ - privateField: Type │ ← Attributes (visibility + name + type)
│ + publicField: Type │
├─────────────────────────┤
│ + publicMethod(): Type │ ← Methods (visibility + signature)
│ - privateHelper(): void │
└─────────────────────────┘
Visibility: + public, - private, # protected.
Relationships with correct arrows:
- Association: solid line (
────) - Aggregation: hollow diamond on whole side (
◇────) - Composition: filled diamond on whole side (
◆────) - Inheritance: solid line + hollow triangle to parent (
──▷) - Implementation: dashed line + hollow triangle to interface (
┄┄▷) - Dependency: dashed arrow (
┄┄►)
Interview drawing strategy:
- Start with 3–4 core classes as boxes.
- Add 2–3 key attributes and methods per class (skip obvious getters).
- Draw relationships with correct arrow types.
- Add multiplicity (1, , 1..) to every relationship line.
- Expand with more classes only if time permits or interviewer asks.
Common mistakes to avoid: Drawing isolated boxes with no relationships, skipping visibility modifiers, putting the diamond on the wrong side, and trying to draw everything before discussing it.
Q8. How do you identify the right classes and relationships from a problem statement?
Why interviewers ask: This tests your analytical ability — translating vague requirements into structured design.
Model answer:
I use the noun-verb-adjective extraction technique:
Step 1: Nouns become candidate classes. From "A library has books that members can borrow and return" → Library, Book, Member.
Step 2: Verbs become candidate methods.
"borrow" → borrowBook(), "return" → returnBook(), "has" → relationship indicator.
Step 3: Adjectives become enums or types.
"overdue", "available", "borrowed" → BookStatus enum.
Step 4: Filter and merge.
Not every noun is a class. "Title" is an attribute of Book, not its own class. "System" is often too vague — rename to something specific like LibraryManager.
Step 5: Determine relationships by asking:
- "Does A own B (B cannot exist alone)?" → Composition
- "Does A have B (B can exist alone)?" → Aggregation
- "Does A know about B?" → Association
- "Is A a type of B?" → Inheritance
- "Does A temporarily use B in a method?" → Dependency
- "Does A fulfill the contract of B?" → Implementation
Example output:
Requirement: "An online store has products organized in categories.
Customers browse products, add them to a shopping cart, and place orders."
Classes: Store, Product, Category, Customer, ShoppingCart, Order, OrderItem
Enums: OrderStatus (PENDING, CONFIRMED, SHIPPED, DELIVERED)
Relations: Store ◆── Product (composition)
Category ◇── Product (aggregation — product exists without category)
Customer ── ShoppingCart (association — one active cart per customer)
Order ◆── OrderItem (composition — items die with order)
Customer ── Order (association — 1 to many)
Advanced (Q9–Q11)
Q9. Design the classes for a Parking Lot System. Walk through your complete approach.
Why interviewers ask: This is one of the most common LLD interview questions. It tests entity identification, relationship modeling, OOP application, and edge case thinking.
Model answer:
Requirements (after clarification):
- Multiple floors, each with parking spots of sizes: SMALL, MEDIUM, LARGE.
- Vehicle types: Motorcycle, Car, Truck.
- Entrance panels issue tickets; exit panels process payment.
- Display board shows available spots per floor.
Core Classes:
Enums: VehicleType, SpotSize, TicketStatus
Classes:
┌──────────────┐ ◆ 1..* ┌──────────────┐ ◆ 1..* ┌──────────────┐
│ ParkingLot │──────────│ ParkingFloor │──────────│ ParkingSpot │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ - name │ │ - floorNumber│ │ - spotId │
│ - floors[] │ │ - spots[] │ │ - size │
│ - tickets │ ├──────────────┤ │ - isOccupied │
├──────────────┤ │ +findAvail() │ │ - vehicle? │
│ +enterVeh() │ │ +getCount() │ ├──────────────┤
│ +exitVeh() │ └──────────────┘ │ +canFit(v) │
│ +isFull() │ │ +park(v) │
└──────────────┘ │ +unpark() │
└──────────────┘
┌──────────────┐ ┌────────────────────┐
│ <<abstract>> │ │ <<interface>> │
│ Vehicle │ │ PaymentStrategy │
├──────────────┤ ├────────────────────┤
│ - plate: str │ │ +calculate(ticket) │
│ - type: enum │ │ : number │
├──────────────┤ └────────▲───────────┘
│ +getPlate() │ ┆
│ +getType() │ ┌─────┴──────┐
└──────▲───────┘ │ │
│ extends ┌─────┴──┐ ┌────┴─────┐
┌────┼─────┐ │ Hourly │ │ FlatRate │
│ │ │ └────────┘ └──────────┘
┌─┴──┐┌┴───┐┌┴─────┐
│Car ││Moto││Truck │
└────┘└────┘└──────┘
Key method — enterVehicle:
enterVehicle(vehicle: Vehicle): ParkingTicket {
for (const floor of this.floors) {
const spot = floor.findAvailableSpot(vehicle);
if (spot) {
spot.park(vehicle);
const ticket = new ParkingTicket(vehicle, spot);
this.activeTickets.set(vehicle.getLicensePlate(), ticket);
return ticket;
}
}
throw new Error("Lot is full");
}
Edge cases:
- Concurrent entry — Lock spot assignment to prevent double-booking.
- Lost ticket — Charge max daily rate, verify via license plate cameras.
- Vehicle already inside — Check activeTickets map before allowing entry.
- Pricing flexibility — Strategy pattern for hourly/flat/weekend rates.
- Display board — Observer pattern to update when spot count changes.
Q10. How do design patterns relate to LLD? Name three patterns you use most often and explain when each applies.
Why interviewers ask: Design patterns demonstrate that you can apply proven solutions rather than reinventing the wheel.
Model answer:
Design patterns are reusable solutions to common LLD problems. They are the vocabulary of experienced designers — when I say "I would use the Strategy pattern here," every engineer instantly understands the structure I am proposing.
Three patterns I use most:
1. Strategy Pattern — When behavior needs to vary at runtime.
Use when you have multiple algorithms for the same task and need to switch between them. Classic sign: an if/else or switch chain that selects different behavior.
// Instead of: if (type === "hourly") { ... } else if (type === "flat") { ... }
interface PricingStrategy {
calculate(hours: number): number;
}
class HourlyPricing implements PricingStrategy {
calculate(hours: number): number { return hours * 5; }
}
class FlatPricing implements PricingStrategy {
calculate(hours: number): number { return 20; }
}
// Context uses the strategy without knowing which one
class ParkingPayment {
constructor(private strategy: PricingStrategy) {}
getAmount(hours: number): number { return this.strategy.calculate(hours); }
}
2. Observer Pattern — When multiple objects need to react to a state change.
Use when one object changes and several others need to know, but you do not want tight coupling between them.
interface Observer {
update(data: unknown): void;
}
class DisplayBoard implements Observer {
update(data: unknown): void { console.log("Display updated:", data); }
}
class ParkingLot {
private observers: Observer[] = [];
subscribe(observer: Observer) { this.observers.push(observer); }
private notify(data: unknown) {
this.observers.forEach(o => o.update(data));
}
}
3. Factory Pattern — When object creation is complex or type-dependent.
Use when you need to create objects of different types based on input, without exposing the creation logic to the caller.
class VehicleFactory {
static create(type: VehicleType, plate: string): Vehicle {
switch (type) {
case VehicleType.CAR: return new Car(plate);
case VehicleType.MOTORCYCLE: return new Motorcycle(plate);
case VehicleType.TRUCK: return new Truck(plate);
default: throw new Error(`Unknown vehicle type: ${type}`);
}
}
}
In interviews: I mention patterns naturally — "I would use Strategy here because pricing needs to vary" rather than forcing patterns where they do not fit.
Q11. Critique this class diagram. What would you change and why?
Why interviewers ask: This tests whether you can identify design flaws — not just create designs, but evaluate and improve them.
The diagram to critique:
┌──────────────────────┐
│ User │
├──────────────────────┤
│ + name: string │
│ + email: string │
│ + password: string │
│ + orders: Order[] │
│ + cart: CartItem[] │
│ + addresses: Address[]│
│ + paymentMethods[] │
│ + wishlist: Product[]│
│ + reviewsWritten[] │
│ + notifications[] │
├──────────────────────┤
│ + login() │
│ + register() │
│ + placeOrder() │
│ + addToCart() │
│ + removeFromCart() │
│ + addAddress() │
│ + processPayment() │
│ + writeReview() │
│ + sendNotification() │
│ + generateInvoice() │
│ + applyDiscount() │
└──────────────────────┘
Model answer — Identified problems:
1. God Class / Single Responsibility Violation.
This User class has 11+ fields and 11+ methods spanning authentication, shopping, payments, reviews, notifications, and invoicing. It should be split into at least 5 classes.
2. Encapsulation violation.
All fields are public (+). password especially must be private and should be in an AuthService, not exposed on the User object.
3. Mixed concerns.
processPayment(), sendNotification(), and generateInvoice() are not behaviors of a User. They belong to PaymentService, NotificationService, and InvoiceService respectively.
4. No relationships shown. Where are Order, Product, Review, Address as separate classes? Everything is inlined into User.
Proposed redesign:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User │ │ AuthService │ │ CartService │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ - id │ │ +login() │ │ - items[] │
│ - name │ │ +register() │ │ +addItem() │
│ - email │ │ +hashPwd() │ │ +removeItem()│
├──────────────┤ └──────────────┘ │ +getTotal() │
│ +getName() │ └──────────────┘
│ +getEmail() │ ┌──────────────┐
└──────────────┘ │ OrderService │
├──────────────┤
┌──────────────┐ │ +placeOrder()│
│PaymentService│ │ +getOrders() │
├──────────────┤ └──────────────┘
│ +process() │
│ +refund() │ ┌──────────────┐
└──────────────┘ │ReviewService │
├──────────────┤
┌──────────────┐ │ +write() │
│ Notification │ │ +getForProduct│
│ Service │ └──────────────┘
├──────────────┤
│ +send() │
│ +getHistory()│
└──────────────┘
Key improvements: Single responsibility per class, proper encapsulation, clear relationships, and each service can be tested independently.
Interview Tips
- Start by saying "Let me clarify the requirements first" — this immediately signals a structured approach.
- Draw as you talk — a class diagram on the whiteboard anchors the conversation and prevents miscommunication.
- Name your relationships — say "This is composition because the room cannot exist without the house" rather than just drawing a line.
- Mention design patterns by name — "I would apply the Strategy pattern here for flexible pricing" — even if you do not fully implement it.
- Raise edge cases proactively — "One thing I want to address is concurrency — what happens if two users try to book the same seat?" This shows production-level thinking.
- Trade-offs over perfection — "I chose composition here. The trade-off is less flexibility, but we get guaranteed lifecycle management."
- Use consistent visibility — default fields to
private, methods topublic. Explicitly justify anyprotectedorpublicfield.
Use this doc alongside practice: pick one LLD problem per day, set a 45-minute timer, and practice the full 6-step framework. Record yourself to identify verbal patterns and hesitation points.
← Back to 9.1 — LLD Foundations (README)