Low-Level Design (LLD) interviews demand more than just textbook definitions. They require you to write clean, maintainable, and robust code. Let us dive deep into the real-world application of OOP.
1. Encapsulation: The Bank Account
Encapsulation is not just about making fields private. It is about protecting the **invariants** of your object. A bank account balance should never be directly modified. It must go through controlled logic (deposits, withdrawals) that enforces rules like overdraft limits or thread safety.
typescript
class BankAccount {
// Hidden from the outside world.
private balance: number;
private readonly accountNumber: string;
private readonly overdraftLimit: number;
constructor(accountNumber: string, initialBalance: number = 0, overdraftLimit: number = 0) {
if (initialBalance < 0) throw new Error("Initial balance cannot be negative.");
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.overdraftLimit = overdraftLimit;
}
// Controlled public behavior
public deposit(amount: number): void {
if (amount <= 0) throw new Error("Deposit must be positive.");
this.balance += amount;
}
public withdraw(amount: number): void {
if (amount <= 0) throw new Error("Withdrawal must be positive.");
if (this.balance - amount < -this.overdraftLimit) {
throw new Error("Insufficient funds. Overdraft limit exceeded.");
}
this.balance -= amount;
}
public getBalance(): number {
return this.balance;
}
}2. Composition over Inheritance: The Car & Engine
Inheritance implies an "IS-A" relationship, which creates tight coupling and fragility. Composition implies a "HAS-A" relationship, offering flexibility. Do not create `ElectricCar` and `GasCar` subclasses. Instead, inject an `IEngine` into a generic `Car`. This is Dependency Injection at work.
typescript
// The Abstraction Contract
interface IEngine {
start(): void;
stop(): void;
getFuelType(): string;
}
// Concrete Implementations
class V8Engine implements IEngine {
start(): void { console.log("V8 roaring to life!"); }
stop(): void { console.log("V8 stopped."); }
getFuelType(): string { return "Petrol"; }
}
class ElectricMotor implements IEngine {
start(): void { console.log("Electric motor humming silently."); }
stop(): void { console.log("Power disconnected."); }
getFuelType(): string { return "Electric"; }
}
// Composition: The Car "has" an engine.
class Car {
private engine: IEngine; // Depends on abstraction!
constructor(engine: IEngine) {
this.engine = engine;
}
public turnOn(): void {
console.log("Starting the car...");
this.engine.start();
}
}Pro Tip
If you use Inheritance, what happens when you build a Hybrid Car? Do you inherit from both? Do you duplicate code? Composition completely bypasses the fragile base class problem.
3. Abstraction & Interfaces: The Printer Queue
Abstraction separates *what* a system does from *how* it does it. In LLD, always define the boundaries of your system using Interfaces before writing actual implementations.
typescript
interface IPrinterTask {
getDocumentName(): string;
getPageCount(): number;
}
interface IPrinterNode {
print(task: IPrinterTask): boolean;
getInkLevel(): number;
getStatus(): 'IDLE' | 'PRINTING' | 'ERROR';
}
class LaserPrinter implements IPrinterNode {
private inkLevel = 100;
private status: 'IDLE' | 'PRINTING' | 'ERROR' = 'IDLE';
public print(task: IPrinterTask): boolean {
if (this.inkLevel < task.getPageCount() * 0.1) {
this.status = 'ERROR';
return false;
}
this.status = 'PRINTING';
this.inkLevel -= task.getPageCount() * 0.1;
// Print logic...
this.status = 'IDLE';
return true;
}
public getInkLevel(): number { return this.inkLevel; }
public getStatus() { return this.status; }
}Take the Phase Interview
Solidify your knowledge. We will ask you 5 random highly-technical questions from the Phase 1: OOP Foundations bucket. A Staff Engineer AI will strictly evaluate your answers.