OOP essentials
Every LLD answer is built from five ideas: encapsulation, abstraction, inheritance, composition and polymorphism. You know the words; this module shows what each one buys you in an interview, and the inheritance mistake that most designs make.
1. The idea
| Idea | In one line | Why interviewers care |
|---|---|---|
| Encapsulation | Data is private; methods enforce the rules | An account's balance can't go negative by accident |
| Abstraction | Callers see what an object does, not how | Code depends on "can pay", not on "is a Visa card" |
| Inheritance | A subclass is a special kind of its parent ("is-a") | Useful, but couples tightly; most misuse happens here |
| Composition | An object is built from others it holds ("has-a") | Flexible; prefer it to inheritance |
| Polymorphism | One call, many behaviours, chosen by the object | Replaces if/else on types |
The most quoted rule from the Design Patterns book is "favour object composition over class inheritance". Inherit only when the child truly is the parent and can stand in for it everywhere (Module 03, Liskov).
2. How it works
The same checkout() call, two different objects. Watch the call travel to whichever implementation the order was handed.
3. Try it yourself
Is-a or has-a?
For each pair, choose inheritance (is-a) or composition (has-a). Then check.
4. In code
All five ideas in one small program: an encapsulated account, a payment interface with two implementations, and an order composed of line items.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Account:
"""Encapsulation: the balance can only change through methods that enforce the rules."""
def __init__(self, owner: str, balance: int = 0) -> None:
self.owner = owner
self._balance = balance # private by convention: no one outside sets it
@property
def balance(self) -> int:
return self._balance
def withdraw(self, amount: int) -> None:
if amount <= 0:
raise ValueError("amount must be positive")
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance -= amount
class PaymentMethod(ABC):
"""Abstraction: callers know 'it can pay', not how."""
@abstractmethod
def pay(self, amount: int) -> str: ...
class CardPayment(PaymentMethod):
def __init__(self, last4: str) -> None:
self.last4 = last4
def pay(self, amount: int) -> str:
return f"charged {amount} to card ending {self.last4}"
class WalletPayment(PaymentMethod):
def __init__(self, account: Account) -> None:
self.account = account # composition: a wallet HAS an account; it isn't one
def pay(self, amount: int) -> str:
self.account.withdraw(amount)
return f"paid {amount} from {self.account.owner}'s wallet, {self.account.balance} left"
@dataclass(frozen=True)
class LineItem:
sku: str
price: int
qty: int
class Order:
"""Composition: line items live and die with their order."""
def __init__(self) -> None:
self.items: list[LineItem] = []
def add(self, sku: str, price: int, qty: int = 1) -> Order:
self.items.append(LineItem(sku, price, qty))
return self
def total(self) -> int:
return sum(i.price * i.qty for i in self.items)
def checkout(self, method: PaymentMethod) -> str:
return method.pay(self.total()) # polymorphism: works for any PaymentMethod
if __name__ == "__main__":
order = Order().add("book", 30).add("pen", 5, 2)
print("total:", order.total())
wallet = Account("ana", 100)
for method in (CardPayment("4242"), WalletPayment(wallet)):
print(order.checkout(method))
try:
WalletPayment(wallet).pay(500)
except ValueError as e:
print("wallet:", e)
/** Encapsulation: the balance can only change through methods that enforce the rules. */
class Account {
#balance: number; // truly private: no one outside can set it
constructor(readonly owner: string, balance = 0) { this.#balance = balance; }
get balance(): number { return this.#balance; }
withdraw(amount: number): void {
if (amount <= 0) throw new Error("amount must be positive");
if (amount > this.#balance) throw new Error("insufficient funds");
this.#balance -= amount;
}
}
/** Abstraction: callers know "it can pay", not how. */
interface PaymentMethod { pay(amount: number): string; }
class CardPayment implements PaymentMethod {
constructor(private last4: string) {}
pay(amount: number): string { return `charged ${amount} to card ending ${this.last4}`; }
}
class WalletPayment implements PaymentMethod {
constructor(private account: Account) {} // composition: a wallet HAS an account; it isn't one
pay(amount: number): string {
this.account.withdraw(amount);
return `paid ${amount} from ${this.account.owner}'s wallet, ${this.account.balance} left`;
}
}
interface LineItem { sku: string; price: number; qty: number; }
/** Composition: line items live and die with their order. */
class Order {
private items: LineItem[] = [];
add(sku: string, price: number, qty = 1): Order {
this.items.push({ sku, price, qty });
return this;
}
total(): number { return this.items.reduce((s, i) => s + i.price * i.qty, 0); }
checkout(method: PaymentMethod): string { return method.pay(this.total()); } // polymorphism: works for any PaymentMethod
}
const order = new Order().add("book", 30).add("pen", 5, 2);
console.log("total:", order.total());
const wallet = new Account("ana", 100);
for (const method of [new CardPayment("4242"), new WalletPayment(wallet)]) console.log(order.checkout(method));
try {
new WalletPayment(wallet).pay(500);
} catch (e) {
console.log("wallet:", (e as Error).message);
}import java.util.ArrayList;
import java.util.List;
/** Encapsulation: the balance can only change through methods that enforce the rules. */
class Account {
final String owner;
private int balance; // private: no one outside can set it
Account(String owner, int balance) {
this.owner = owner;
this.balance = balance;
}
int balance() {
return balance;
}
synchronized void withdraw(int amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
if (amount > balance) throw new IllegalArgumentException("insufficient funds");
balance -= amount;
}
}
/** Abstraction: callers know "it can pay", not how. */
interface PaymentMethod {
String pay(int amount);
}
class CardPayment implements PaymentMethod {
private final String last4;
CardPayment(String last4) {
this.last4 = last4;
}
public String pay(int amount) {
return "charged " + amount + " to card ending " + last4;
}
}
class WalletPayment implements PaymentMethod {
private final Account account; // composition: a wallet HAS an account; it isn't one
WalletPayment(Account account) {
this.account = account;
}
public String pay(int amount) {
account.withdraw(amount);
return "paid " + amount + " from " + account.owner + "'s wallet, " + account.balance() + " left";
}
}
record LineItem(String sku, int price, int qty) {}
/** Composition: line items live and die with their order. */
class Order {
private final List<LineItem> items = new ArrayList<>();
Order add(String sku, int price, int qty) {
items.add(new LineItem(sku, price, qty));
return this;
}
int total() {
return items.stream().mapToInt(i -> i.price() * i.qty()).sum();
}
String checkout(PaymentMethod method) {
return method.pay(total()); // polymorphism: works for any PaymentMethod
}
}
public class Main {
public static void main(String[] args) {
Order order = new Order().add("book", 30, 1).add("pen", 5, 2);
System.out.println("total: " + order.total());
Account wallet = new Account("ana", 100);
for (PaymentMethod m : List.of(new CardPayment("4242"), new WalletPayment(wallet))) System.out.println(order.checkout(m));
try {
new WalletPayment(wallet).pay(500);
} catch (IllegalArgumentException e) {
System.out.println("wallet: " + e.getMessage());
}
}
}Output (the same in all three languages)
total: 40 charged 40 to card ending 4242 paid 40 from ana's wallet, 60 left wallet: insufficient funds
5. In the interview
- Make fields private by default and expose behaviour, not setters. "account.withdraw(50)" can enforce rules; "account.setBalance(x)" can't.
- Say why you chose inheritance when you do, and prefer an interface plus composition when the answer is "to reuse some code".
- Use polymorphism to remove type checks. If you write
if type == "card", an interface is missing. - Enums for fixed sets (sizes, states, directions), classes for things with behaviour.
6. Remember it
How this hook works: what “Hide the data, show the behaviour, hold rather than be” means
What the slogan means, phrase by phrase
- “Hide the data”
- Fields are private; only methods change them, and they enforce the rules.
- “show the behaviour”
- Callers use an interface: what the object does, not how.
- “hold rather than be”
- Build objects from parts they hold (has-a) more often than from parents they extend (is-a).
Why this picture fits: The car: a sealed engine (hidden data), the same pedals in every model (behaviour), and an engine it has rather than is.
The story → the concept
| The car | OOP |
|---|---|
| Sealed engine bay | Encapsulation |
| Pedals and wheel | The interface |
| Any car drives the same way | Polymorphism |
| Has an engine | Composition |
| A sports car is a car | Inheritance, used where it's true |
Watch it on an example
- Hide: the wallet's balance only changes through withdraw(), which refuses overdrafts.
- Show: Order.checkout() takes any PaymentMethod.
- Hold: WalletPayment holds an Account; it doesn't extend it.
The trap it saves you from
Deep inheritance trees built for code reuse, which break when a parent changes.
It still works when…
The language has no classes (Go, or functional style). The same ideas hold with structs, interfaces and closures.
7. Common mistakes
8. Practice exercise (25 minutes)
- Encapsulate (5 min). Add a deposit() with its own rule to the Account in the code.
- Add a payment method (5 min). Gift cards: implement PaymentMethod. Did Order change?
- Spot the inheritance (10 min). Find an extends in code you know. Is it really is-a? Could it be has-a?
- Explain it (5 min). Out loud: the five ideas, one sentence each, with an example.
9. Self-check quiz
Which is the best example of encapsulation?
Stack and ArrayList: is-a or has-a?
What does polymorphism replace?
What's the usual guidance on inheritance vs composition?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Module 03: SOLID in practice | Next | Five principles that build on these ideas. |
| refactoring.guru: design patterns | Reference | Clear illustrated explanations, with code in many languages. |
| P07: Vending machine | 45 min | Polymorphism replacing a state switch. |