Episode 9 — System Design / 9.1 — LLD Foundations

9.1.b — OOP Fundamentals

In one sentence: Object-Oriented Programming organizes code around objects (data + behavior bundled together), giving us encapsulation, inheritance, polymorphism, and abstraction — the four pillars that make large codebases manageable.


Table of Contents


1. What Is OOP?

Object-Oriented Programming is a programming paradigm that models software as a collection of objects, each containing:

  • State (data / properties / attributes)
  • Behavior (methods / functions)

Instead of writing long procedural scripts where functions operate on loose data, OOP bundles related data and behavior together inside objects.

┌─────────────────────────────────────────────────────────┐
│                PROCEDURAL vs OOP                        │
│                                                         │
│  Procedural:                                            │
│  ─────────────                                          │
│  Data and functions are separate.                       │
│  Functions operate on data passed to them.              │
│                                                         │
│    let name = "Alice";                                  │
│    let balance = 100;                                   │
│    function deposit(amount) { balance += amount; }      │
│                                                         │
│  OOP:                                                   │
│  ────                                                   │
│  Data and functions are bundled into objects.            │
│  Objects manage their own state.                        │
│                                                         │
│    class Account {                                      │
│      name = "Alice";                                    │
│      balance = 100;                                     │
│      deposit(amount) { this.balance += amount; }        │
│    }                                                    │
└─────────────────────────────────────────────────────────┘

2. Classes and Objects

A class is a blueprint. An object is an instance of that blueprint.

Class = Blueprint                    Object = Instance
┌──────────────────┐                 ┌──────────────────┐
│     Car          │                 │  myCar            │
│  ──────────────  │    new Car()    │  ───────────────  │
│  brand: string   │ ──────────────► │  brand: "Toyota"  │
│  speed: number   │                 │  speed: 0         │
│  accelerate()    │                 │  accelerate()     │
│  brake()         │                 │  brake()          │
└──────────────────┘                 └──────────────────┘
                                     ┌──────────────────┐
                        new Car()    │  yourCar          │
                     ──────────────► │  ───────────────  │
                                     │  brand: "Honda"   │
                                     │  speed: 0         │
                                     │  accelerate()     │
                                     │  brake()          │
                                     └──────────────────┘

JavaScript Example

class Car {
  constructor(brand, year) {
    this.brand = brand;
    this.year = year;
    this.speed = 0;
  }

  accelerate(amount) {
    this.speed += amount;
    console.log(`${this.brand} is now going ${this.speed} km/h`);
  }

  brake(amount) {
    this.speed = Math.max(0, this.speed - amount);
    console.log(`${this.brand} slowed to ${this.speed} km/h`);
  }

  getInfo() {
    return `${this.year} ${this.brand} — current speed: ${this.speed} km/h`;
  }
}

// Creating objects (instances)
const myCar = new Car("Toyota", 2023);
const yourCar = new Car("Honda", 2022);

myCar.accelerate(60);   // "Toyota is now going 60 km/h"
yourCar.accelerate(80);  // "Honda is now going 80 km/h"
myCar.brake(20);         // "Toyota slowed to 40 km/h"

TypeScript Example (with types)

class Car {
  private brand: string;
  private year: number;
  private speed: number;

  constructor(brand: string, year: number) {
    this.brand = brand;
    this.year = year;
    this.speed = 0;
  }

  accelerate(amount: number): void {
    this.speed += amount;
  }

  brake(amount: number): void {
    this.speed = Math.max(0, this.speed - amount);
  }

  getSpeed(): number {
    return this.speed;
  }

  getInfo(): string {
    return `${this.year} ${this.brand} — current speed: ${this.speed} km/h`;
  }
}

3. The Four Pillars of OOP

┌─────────────────────────────────────────────────────────┐
│              THE FOUR PILLARS OF OOP                     │
│                                                         │
│  ┌─────────────┐ ┌─────────────┐ ┌────────────────┐    │
│  │ENCAPSULATION│ │ INHERITANCE │ │ POLYMORPHISM   │    │
│  │             │ │             │ │                │    │
│  │ Hide data   │ │ Reuse code  │ │ Many forms     │    │
│  │ behind      │ │ from parent │ │ of the same    │    │
│  │ methods     │ │ classes     │ │ interface      │    │
│  └─────────────┘ └─────────────┘ └────────────────┘    │
│                                                         │
│                  ┌─────────────┐                        │
│                  │ ABSTRACTION │                        │
│                  │             │                        │
│                  │ Show only   │                        │
│                  │ what matters│                        │
│                  │ hide the    │                        │
│                  │ complexity  │                        │
│                  └─────────────┘                        │
└─────────────────────────────────────────────────────────┘

3.1 Encapsulation

Encapsulation means bundling data and the methods that operate on that data into a single unit (class), and restricting direct access to the internal state.

Why? If any code anywhere can directly change an object's state, bugs become impossible to track. Encapsulation creates controlled access points.

Without Encapsulation:              With Encapsulation:
┌──────────────┐                    ┌──────────────┐
│ BankAccount  │                    │ BankAccount  │
│              │                    │              │
│ balance: 100 │  ← anyone can     │ -balance: 100│  ← private
│              │    set this to     │              │
│              │    -9999           │ +deposit()   │  ← controlled
│              │                    │ +withdraw()  │    access
│              │                    │ +getBalance()│
└──────────────┘                    └──────────────┘

JavaScript Example

class BankAccount {
  #balance; // Private field (ES2022 private fields with #)

  constructor(ownerName, initialBalance) {
    this.ownerName = ownerName;
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error("Deposit must be positive");
    this.#balance += amount;
    return this.#balance;
  }

  withdraw(amount) {
    if (amount <= 0) throw new Error("Withdrawal must be positive");
    if (amount > this.#balance) throw new Error("Insufficient funds");
    this.#balance -= amount;
    return this.#balance;
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount("Alice", 1000);
account.deposit(500);        // OK: balance is now 1500
account.withdraw(200);       // OK: balance is now 1300
console.log(account.getBalance()); // 1300

// account.#balance = 999999; // SyntaxError: Private field
// Encapsulation PREVENTS direct manipulation

TypeScript Example

class BankAccount {
  private balance: number;
  public readonly ownerName: string;

  constructor(ownerName: string, initialBalance: number) {
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  deposit(amount: number): number {
    if (amount <= 0) throw new Error("Deposit must be positive");
    this.balance += amount;
    return this.balance;
  }

  withdraw(amount: number): number {
    if (amount <= 0) throw new Error("Withdrawal must be positive");
    if (amount > this.balance) throw new Error("Insufficient funds");
    this.balance -= amount;
    return this.balance;
  }

  getBalance(): number {
    return this.balance;
  }
}

Access Modifiers in TypeScript:

ModifierWho Can AccessUse When
publicAnyoneThe method is part of the class's public API
privateOnly the class itselfInternal state or helper methods
protectedThe class + its subclassesState that subclasses need but outsiders should not touch
readonlyAnyone (read), only constructor (write)Values set once and never changed

3.2 Inheritance

Inheritance lets a class (child/subclass) reuse the properties and methods of another class (parent/superclass). It models the "is-a" relationship.

         ┌──────────┐
         │  Animal   │   ← Parent / Superclass
         │ ──────── │
         │ name     │
         │ speak()  │
         └────┬─────┘
              │ extends
     ┌────────┼────────┐
     │        │        │
┌────▼───┐ ┌─▼──────┐ ┌▼───────┐
│  Dog   │ │  Cat   │ │  Bird  │  ← Child / Subclass
│ bark() │ │ purr() │ │ fly()  │
└────────┘ └────────┘ └────────┘

JavaScript Example

class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    console.log(`${this.name} says ${this.sound}`);
  }

  eat(food) {
    console.log(`${this.name} is eating ${food}`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name, "Woof!");  // Call parent constructor
  }

  fetch(item) {
    console.log(`${this.name} fetches the ${item}`);
  }

  // Override parent method
  speak() {
    console.log(`${this.name} barks: ${this.sound}`);
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name, "Meow!");
  }

  purr() {
    console.log(`${this.name} is purring...`);
  }
}

const dog = new Dog("Rex");
dog.speak();       // "Rex barks: Woof!" — overridden method
dog.eat("bone");   // "Rex is eating bone" — inherited method
dog.fetch("ball"); // "Rex fetches the ball" — own method

const cat = new Cat("Whiskers");
cat.speak();       // "Whiskers says Meow!" — inherited method
cat.purr();        // "Whiskers is purring..." — own method

TypeScript Example

class Animal {
  protected name: string;
  protected sound: string;

  constructor(name: string, sound: string) {
    this.name = name;
    this.sound = sound;
  }

  speak(): void {
    console.log(`${this.name} says ${this.sound}`);
  }

  eat(food: string): void {
    console.log(`${this.name} is eating ${food}`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name, "Woof!");
  }

  fetch(item: string): void {
    console.log(`${this.name} fetches the ${item}`);
  }

  speak(): void {
    console.log(`${this.name} barks: ${this.sound}`);
  }
}

When to Use Inheritance — and When NOT To

Use Inheritance WhenAvoid Inheritance When
Clear "is-a" relationship (Dog IS an Animal)Relationship is "has-a" (Car HAS an Engine)
Child shares most of the parent's behaviorChild only needs a small part of the parent
The hierarchy is shallow (2–3 levels max)Deep hierarchies (5+ levels) — becomes brittle
Behavior truly specializes the parentYou just want code reuse (use composition instead)

Rule of thumb: Favor composition over inheritance. If in doubt, compose; if the relationship is genuinely "is-a", inherit.


3.3 Polymorphism

Polymorphism means "many forms" — the same interface or method name behaves differently depending on the object that implements it. There are two types:

┌───────────────────────────────────────────────────────┐
│                   POLYMORPHISM                        │
│                                                       │
│   ┌─────────────────────┐  ┌───────────────────────┐  │
│   │  COMPILE-TIME        │  │  RUNTIME              │  │
│   │  (Static)            │  │  (Dynamic)            │  │
│   │                      │  │                       │  │
│   │  Method Overloading  │  │  Method Overriding    │  │
│   │  Same name,          │  │  Same name & params,  │  │
│   │  different params    │  │  different class       │  │
│   │                      │  │  (via inheritance)    │  │
│   │  Resolved at         │  │  Resolved at          │  │
│   │  compile time        │  │  runtime              │  │
│   └─────────────────────┘  └───────────────────────┘  │
└───────────────────────────────────────────────────────┘

Compile-Time Polymorphism (Method Overloading)

TypeScript supports overloading via overload signatures (JavaScript does not have native overloading):

class Calculator {
  // Overload signatures
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: number, b: number, c: number): number;

  // Implementation signature
  add(a: number | string, b: number | string, c?: number): number | string {
    if (typeof a === "string" && typeof b === "string") {
      return a + b; // string concatenation
    }
    if (typeof a === "number" && typeof b === "number") {
      return c ? a + b + c : a + b;
    }
    throw new Error("Invalid arguments");
  }
}

const calc = new Calculator();
console.log(calc.add(1, 2));           // 3 (number + number)
console.log(calc.add("Hello ", "World")); // "Hello World" (string + string)
console.log(calc.add(1, 2, 3));        // 6 (three numbers)

Runtime Polymorphism (Method Overriding)

The same method name behaves differently depending on which object calls it. The decision is made at runtime.

class Shape {
  area() {
    throw new Error("area() must be implemented by subclass");
  }

  describe() {
    console.log(`This shape has an area of ${this.area().toFixed(2)}`);
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

class Triangle extends Shape {
  constructor(base, height) {
    super();
    this.base = base;
    this.height = height;
  }

  area() {
    return 0.5 * this.base * this.height;
  }
}

// Polymorphism in action: same interface, different behavior
const shapes = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(3, 8),
];

shapes.forEach(shape => shape.describe());
// "This shape has an area of 78.54"
// "This shape has an area of 24.00"
// "This shape has an area of 12.00"

The power: The forEach loop does not know or care which specific shape it is dealing with. It calls describe() and the correct area() runs automatically. This is runtime polymorphism.

TypeScript Interface Polymorphism

interface Notifier {
  send(to: string, message: string): Promise<boolean>;
}

class EmailNotifier implements Notifier {
  async send(to: string, message: string): Promise<boolean> {
    console.log(`Sending email to ${to}: ${message}`);
    return true;
  }
}

class SMSNotifier implements Notifier {
  async send(to: string, message: string): Promise<boolean> {
    console.log(`Sending SMS to ${to}: ${message}`);
    return true;
  }
}

class PushNotifier implements Notifier {
  async send(to: string, message: string): Promise<boolean> {
    console.log(`Sending push notification to ${to}: ${message}`);
    return true;
  }
}

// The calling code does not know which notifier it uses
async function notifyUser(notifier: Notifier, user: string, msg: string) {
  await notifier.send(user, msg);
}

// At runtime, any Notifier implementation works
await notifyUser(new EmailNotifier(), "alice@example.com", "Hello!");
await notifyUser(new SMSNotifier(), "+1234567890", "Hello!");
await notifyUser(new PushNotifier(), "device_token_abc", "Hello!");

3.4 Abstraction

Abstraction means exposing only the essential details and hiding the complexity. Users of a class should know what it does, not how it does it.

Real-world analogy: When you drive a car, you use the steering wheel, pedals, and gear shift. You do not need to understand how the internal combustion engine works. The car abstracts its internals behind a simple interface.

┌─────────────────────────────────────────────────────────┐
│                    ABSTRACTION                          │
│                                                         │
│  What the user sees:         What is hidden:            │
│  ┌─────────────────┐         ┌─────────────────┐        │
│  │ database.save() │         │ Connection pool  │        │
│  │ database.find() │         │ Query building   │        │
│  │ database.delete()         │ Transaction mgmt │        │
│  └─────────────────┘         │ Error retry      │        │
│                               │ Caching layer    │        │
│  Simple interface             │ Logging          │        │
│  (abstraction layer)          └─────────────────┘        │
│                               Complex implementation     │
└─────────────────────────────────────────────────────────┘

TypeScript Abstract Class Example

// Abstract class — cannot be instantiated directly
abstract class Database {
  // Abstract methods — subclasses MUST implement these
  abstract connect(): Promise<void>;
  abstract disconnect(): Promise<void>;
  abstract query(sql: string, params?: unknown[]): Promise<unknown[]>;

  // Concrete method — shared behavior, available to all subclasses
  async findById(table: string, id: string): Promise<unknown | null> {
    const results = await this.query(
      `SELECT * FROM ${table} WHERE id = ?`,
      [id]
    );
    return results[0] ?? null;
  }

  async save(table: string, data: Record<string, unknown>): Promise<void> {
    const keys = Object.keys(data).join(", ");
    const placeholders = Object.keys(data).map(() => "?").join(", ");
    await this.query(
      `INSERT INTO ${table} (${keys}) VALUES (${placeholders})`,
      Object.values(data)
    );
  }
}

// Concrete implementation for PostgreSQL
class PostgresDatabase extends Database {
  private connectionString: string;

  constructor(connectionString: string) {
    super();
    this.connectionString = connectionString;
  }

  async connect(): Promise<void> {
    console.log(`Connecting to PostgreSQL at ${this.connectionString}`);
    // ... actual pg connection logic
  }

  async disconnect(): Promise<void> {
    console.log("Disconnecting from PostgreSQL");
    // ... actual pg disconnect logic
  }

  async query(sql: string, params?: unknown[]): Promise<unknown[]> {
    console.log(`[PG] Executing: ${sql}`);
    // ... actual pg query logic
    return [];
  }
}

// Concrete implementation for MongoDB
class MongoDatabase extends Database {
  private uri: string;

  constructor(uri: string) {
    super();
    this.uri = uri;
  }

  async connect(): Promise<void> {
    console.log(`Connecting to MongoDB at ${this.uri}`);
  }

  async disconnect(): Promise<void> {
    console.log("Disconnecting from MongoDB");
  }

  async query(sql: string, params?: unknown[]): Promise<unknown[]> {
    console.log(`[Mongo] Executing: ${sql}`);
    return [];
  }
}

// Application code uses the abstract type — does not care which DB
class UserService {
  private db: Database;

  constructor(db: Database) {
    this.db = db;
  }

  async getUser(id: string) {
    return this.db.findById("users", id);
  }
}

// Swap databases without changing UserService
const userServicePG = new UserService(new PostgresDatabase("pg://localhost"));
const userServiceMongo = new UserService(new MongoDatabase("mongodb://localhost"));

Abstract Class vs Interface

FeatureAbstract ClassInterface
Can have implementationYes — concrete methods with logicNo — only method signatures (in pure form)
Can have stateYes — properties with valuesNo (in TypeScript interfaces, no runtime state)
InheritanceSingle parent class onlyA class can implement multiple interfaces
ConstructorYesNo
Use whenShared behavior + forced contractPure contract, multiple implementations expected
// Interface: pure contract
interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

// Abstract class: shared behavior + contract
abstract class BaseEntity {
  id: string;
  createdAt: Date;

  constructor() {
    this.id = crypto.randomUUID();
    this.createdAt = new Date();
  }

  abstract validate(): boolean; // subclasses must implement
  
  getAge(): number { // shared implementation
    return Date.now() - this.createdAt.getTime();
  }
}

// A class can extend ONE abstract class and implement MANY interfaces
class User extends BaseEntity implements Serializable {
  name: string;
  email: string;

  constructor(name: string, email: string) {
    super();
    this.name = name;
    this.email = email;
  }

  validate(): boolean {
    return this.name.length > 0 && this.email.includes("@");
  }

  serialize(): string {
    return JSON.stringify({ id: this.id, name: this.name, email: this.email });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.name = parsed.name;
    this.email = parsed.email;
  }
}

4. OOP vs Procedural Programming

AspectProceduralObject-Oriented
OrganizationFunctions + global/local dataClasses + encapsulated data
Data exposureData is often globally accessibleData is private, accessed via methods
Code reuseCopy-paste or function librariesInheritance, composition, interfaces
ScalabilityHard to manage beyond ~1000 linesScales well to large codebases
TestingHarder — functions depend on shared stateEasier — objects are self-contained units
ModelingThink in terms of "steps"Think in terms of "entities and interactions"
ExamplesC, early PHP, Bash scriptsTypeScript, Java, C#, Python (OOP features)

Side-by-Side Code Comparison

// ========== PROCEDURAL APPROACH ==========
let users = [];

function createUser(name, email) {
  const user = { name, email, balance: 0 };
  users.push(user);
  return user;
}

function deposit(user, amount) {
  if (amount <= 0) return;
  user.balance += amount;
}

function withdraw(user, amount) {
  if (amount > user.balance) return;
  user.balance -= amount;
}

function getBalance(user) {
  return user.balance;
}

// Problem: anyone can do `user.balance = 999999` — no protection
const alice = createUser("Alice", "alice@mail.com");
alice.balance = 999999; // oops — no encapsulation
// ========== OOP APPROACH ==========
class User {
  private name: string;
  private email: string;
  private balance: number;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
    this.balance = 0;
  }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error("Invalid amount");
    this.balance += amount;
  }

  withdraw(amount: number): void {
    if (amount <= 0) throw new Error("Invalid amount");
    if (amount > this.balance) throw new Error("Insufficient funds");
    this.balance -= amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const alice = new User("Alice", "alice@mail.com");
// alice.balance = 999999; // TypeScript error: 'balance' is private
alice.deposit(500); // Controlled access

5. OOP Principles Applied to Real Systems

5.1 E-Commerce Order System

// Encapsulation — Order hides pricing logic
class Order {
  private items: OrderItem[] = [];
  private discount: number = 0;

  addItem(item: OrderItem): void {
    this.items.push(item);
  }

  applyDiscount(percentage: number): void {
    if (percentage < 0 || percentage > 100) {
      throw new Error("Invalid discount");
    }
    this.discount = percentage;
  }

  getTotal(): number {
    const subtotal = this.items.reduce(
      (sum, item) => sum + item.getPrice() * item.getQuantity(), 0
    );
    return subtotal * (1 - this.discount / 100);
  }
}

// Inheritance — different order types
class SubscriptionOrder extends Order {
  private renewalDate: Date;

  constructor(renewalDate: Date) {
    super();
    this.renewalDate = renewalDate;
  }

  isRenewalDue(): boolean {
    return new Date() >= this.renewalDate;
  }
}

// Polymorphism — different payment methods, same interface
interface PaymentMethod {
  charge(amount: number): Promise<PaymentReceipt>;
  getName(): string;
}

class CreditCardPayment implements PaymentMethod {
  async charge(amount: number): Promise<PaymentReceipt> {
    // Credit card processing logic
    return { transactionId: "cc_123", amount, method: this.getName() };
  }
  getName(): string { return "Credit Card"; }
}

class WalletPayment implements PaymentMethod {
  async charge(amount: number): Promise<PaymentReceipt> {
    // Digital wallet processing logic
    return { transactionId: "w_456", amount, method: this.getName() };
  }
  getName(): string { return "Digital Wallet"; }
}

// Abstraction — OrderProcessor hides orchestration complexity
class OrderProcessor {
  async processOrder(order: Order, payment: PaymentMethod): Promise<void> {
    const total = order.getTotal();
    const receipt = await payment.charge(total);
    console.log(`Order processed: ${receipt.transactionId} via ${receipt.method}`);
    // ... inventory update, email notification, analytics ...
  }
}

5.2 Logger System (All Four Pillars)

// ABSTRACTION: abstract base class defines the contract
abstract class Logger {
  protected level: LogLevel;

  constructor(level: LogLevel) {
    this.level = level;
  }

  // Template method — shared algorithm, specific steps overridden
  log(level: LogLevel, message: string): void {
    if (level >= this.level) {
      const formatted = this.format(level, message);
      this.write(formatted);
    }
  }

  // Concrete shared method
  protected format(level: LogLevel, message: string): string {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] [${LogLevel[level]}] ${message}`;
  }

  // Abstract — subclasses decide WHERE to write
  protected abstract write(message: string): void;
}

// INHERITANCE: specialized loggers
class ConsoleLogger extends Logger {
  // ENCAPSULATION: write method is protected
  protected write(message: string): void {
    console.log(message);
  }
}

class FileLogger extends Logger {
  private filePath: string;

  constructor(level: LogLevel, filePath: string) {
    super(level);
    this.filePath = filePath;
  }

  protected write(message: string): void {
    // fs.appendFileSync(this.filePath, message + "\n");
    console.log(`[FILE: ${this.filePath}] ${message}`);
  }
}

class HttpLogger extends Logger {
  private endpoint: string;

  constructor(level: LogLevel, endpoint: string) {
    super(level);
    this.endpoint = endpoint;
  }

  protected write(message: string): void {
    // fetch(this.endpoint, { method: 'POST', body: message });
    console.log(`[HTTP: ${this.endpoint}] ${message}`);
  }
}

// POLYMORPHISM: same interface, different destinations
enum LogLevel { DEBUG, INFO, WARN, ERROR }

const loggers: Logger[] = [
  new ConsoleLogger(LogLevel.DEBUG),
  new FileLogger(LogLevel.WARN, "/var/log/app.log"),
  new HttpLogger(LogLevel.ERROR, "https://logging.example.com/ingest"),
];

// All loggers respond to the same method — polymorphism
loggers.forEach(logger => {
  logger.log(LogLevel.ERROR, "Database connection failed");
});

6. Key Takeaways

  1. Classes are blueprints, objects are instances. A class defines structure; new creates a living object from that blueprint.
  2. Encapsulation protects data by making it private and providing controlled access through methods. This prevents invalid states.
  3. Inheritance enables code reuse through "is-a" relationships but should be used sparingly — prefer composition when the relationship is "has-a".
  4. Polymorphism lets different objects respond to the same method call in their own way. Runtime polymorphism (overriding) is the most powerful form for LLD.
  5. Abstraction hides complexity behind simple interfaces. Users of a class should know what it does, not how it does it internally.
  6. OOP is not always better than procedural — use OOP when modeling systems with multiple interacting entities; use procedural for simple scripts and utilities.
  7. TypeScript's type system makes OOP more explicit with access modifiers (private, protected, public), interfaces, and abstract classes.

Explain-It Challenge

Can you explain the difference between encapsulation and abstraction to a junior developer? Encapsulation is about protecting data (private fields + public methods). Abstraction is about hiding complexity (exposing only what users need). Try explaining with the ATM analogy: abstraction = you interact with the screen and buttons (simple interface), encapsulation = the cash vault inside is locked and only the machine's internal logic can access it.


Previous → 9.1.a — What Is LLD Next → 9.1.c — Class Relationships