OOP: The Four Pillars

beginner 0-2 YOE lld oop encapsulation abstraction inheritance polymorphism

Every LLD interview assumes we know OOP cold. These four pillars are the building blocks of everything we’ll design. Let’s break each one down with real-world analogies and code.

1. Encapsulation

Encapsulation means bundling data and the methods that operate on that data into a single unit (a class), and hiding the internal state from the outside world.

Think of it like a TV remote. We press “volume up” and the volume goes up. We don’t need to know about the circuits inside. The remote encapsulates its internal workings and gives us a clean interface (buttons).

class BankAccount:
    def __init__(self, owner, balance):
        self._owner = owner       # "protected" by convention
        self.__balance = balance   # "private" -- name-mangled

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def get_balance(self):
        return self.__balance

account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.get_balance())  # 1500
# account.__balance  # AttributeError -- can't touch it directly
class BankAccount {
  #balance; // private field (ES2022)

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

  deposit(amount) {
    if (amount > 0) this.#balance += amount;
  }

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

const account = new BankAccount("Alice", 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// account.#balance  // SyntaxError -- can't access private field
public class BankAccount {
    private String owner;
    private double balance;

    public BankAccount(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    public void deposit(double amount) {
        if (amount > 0) this.balance += amount;
    }

    public double getBalance() {
        return this.balance;
    }
}
// BankAccount account = new BankAccount("Alice", 1000);
// account.balance  // Compile error -- private

The key idea: nobody outside the class can mess with balance directly. They must go through deposit(). This way we control the rules (no negative deposits, logging, etc.).

2. Abstraction

Abstraction means hiding complex implementation details and exposing only what’s necessary.

In simple language, when we drive a car, we use the steering wheel and pedals. We don’t need to understand fuel injection or the transmission system. The car abstracts all that complexity away.

The only difference between encapsulation and abstraction: encapsulation is about hiding data, abstraction is about hiding complexity.

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount):
        pass  # subclasses MUST implement this

class CreditCardPayment(PaymentProcessor):
    def pay(self, amount):
        # complex credit card logic hidden inside
        print(f"Charged ${amount} to credit card")

class UPIPayment(PaymentProcessor):
    def pay(self, amount):
        # complex UPI logic hidden inside
        print(f"Paid ${amount} via UPI")

# We just call .pay() -- don't care about the internals
payment = UPIPayment()
payment.pay(500)
// JS doesn't have formal abstract classes, but we can simulate
class PaymentProcessor {
  pay(amount) {
    throw new Error("Subclass must implement pay()");
  }
}

class CreditCardPayment extends PaymentProcessor {
  pay(amount) {
    console.log(`Charged $${amount} to credit card`);
  }
}

class UPIPayment extends PaymentProcessor {
  pay(amount) {
    console.log(`Paid $${amount} via UPI`);
  }
}

const payment = new UPIPayment();
payment.pay(500);
abstract class PaymentProcessor {
    abstract void pay(double amount);
}

class CreditCardPayment extends PaymentProcessor {
    void pay(double amount) {
        System.out.println("Charged $" + amount + " to credit card");
    }
}

class UPIPayment extends PaymentProcessor {
    void pay(double amount) {
        System.out.println("Paid $" + amount + " via UPI");
    }
}
// PaymentProcessor p = new UPIPayment();
// p.pay(500);  // We don't care HOW it pays

3. Inheritance

Inheritance lets a class reuse code from a parent class. It models an “is-a” relationship.

Think of it like this: a Dog is an Animal. So Dog inherits common stuff from Animal (like eat(), sleep()) and adds its own behavior (like bark()).

class Animal:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"{self.name} is eating")

class Dog(Animal):
    def bark(self):
        print(f"{self.name} says Woof!")

dog = Dog("Buddy")
dog.eat()   # inherited from Animal
dog.bark()  # Dog's own method
class Animal {
  constructor(name) {
    this.name = name;
  }

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

class Dog extends Animal {
  bark() {
    console.log(`${this.name} says Woof!`);
  }
}

const dog = new Dog("Buddy");
dog.eat();  // inherited from Animal
dog.bark(); // Dog's own method
class Animal {
    String name;

    Animal(String name) { this.name = name; }

    void eat() {
        System.out.println(name + " is eating");
    }
}

class Dog extends Animal {
    Dog(String name) { super(name); }

    void bark() {
        System.out.println(name + " says Woof!");
    }
}
// Dog dog = new Dog("Buddy");
// dog.eat();  // inherited
// dog.bark(); // Dog's own

4. Polymorphism

Polymorphism means same method name, different behavior depending on the object. The word literally means “many forms.”

Think of it like the word “open.” We can open a door, open a file, open a conversation — same word, totally different actions depending on context.

class Shape:
    def area(self):
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# Same method call, different behavior
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.area())  # 78.5, then 24
class Shape {
  area() {
    throw new Error("Must implement area()");
  }
}

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

  area() {
    return 3.14 * this.radius ** 2;
  }
}

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

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

const shapes = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach((s) => console.log(s.area())); // 78.5, then 24
abstract class Shape {
    abstract double area();
}

class Circle extends Shape {
    double radius;
    Circle(double radius) { this.radius = radius; }
    double area() { return 3.14 * radius * radius; }
}

class Rectangle extends Shape {
    double width, height;
    Rectangle(double w, double h) { width = w; height = h; }
    double area() { return width * height; }
}

// Shape[] shapes = {new Circle(5), new Rectangle(4, 6)};
// for (Shape s : shapes) System.out.println(s.area());

The magic here: we loop through a list of Shape objects. We don’t know (or care) if it’s a Circle or Rectangle. We just call .area() and the right implementation runs. This is runtime polymorphism (method overriding).

Quick Recap

PillarWhat It DoesReal-World Analogy
EncapsulationHides internal state, exposes methodsTV remote (buttons hide circuitry)
AbstractionHides complexity behind a simple interfaceCar steering wheel (hides engine)
InheritanceReuses code through parent-child classesDog is an Animal
PolymorphismSame method, different behavior”Open” a door vs “open” a file

These four show up in every LLD interview. Not as direct questions, but through how we design our classes. If our Parking Lot design has good encapsulation, clean abstractions, proper inheritance, and polymorphism where needed — the interviewer knows we get OOP.