OOP Concepts Every Developer Should Master in 2026
OOP Concepts Every Developer Should Master in 2026
Here’s a confession: I used to think I understood OOP because I could define a class and create objects. Then I tried to design a chess game in an interview and realized I had no idea how to model pieces, boards, moves, and rules as a system of interacting objects.
Knowing the syntax of OOP isn’t the same as thinking in objects. And in 2026, with LLD interviews becoming standard at every major tech company, the gap between “I can write a class” and “I can design a system” has never mattered more.
This guide covers the OOP concepts that actually matter—not textbook definitions, but the practical skills you need to build clean, extensible software and ace design interviews.
Who This Is For
This guide is for developers who know the basics of at least one OOP language (Python, Java, TypeScript, C++) but want to go deeper. If you’ve never written a class before, start with our Classes and Objects refresher first.
The 4 Pillars of OOP (With Real Analogies)
You’ve seen these four words a thousand times. Let’s make them stick—not with textbook definitions, but with mental models you’ll actually remember.
1. Encapsulation: The ATM Machine
Pillar 1 Data ProtectionThe analogy: An ATM lets you withdraw money, check your balance, and deposit cash. But you can’t reach inside and grab bills directly. You interact through a defined interface (the screen and buttons), and the internal mechanism is hidden.
In code terms: Encapsulation bundles data and the methods that operate on that data into a single unit (a class), and restricts direct access to the internal state.
class BankAccount: def __init__(self, owner: str, balance: float = 0): self._owner = owner # protected self.__balance = balance # private
def deposit(self, amount: float) -> None: if amount <= 0: raise ValueError("Deposit must be positive") self.__balance += amount
def withdraw(self, amount: float) -> None: if amount > self.__balance: raise ValueError("Insufficient funds") self.__balance -= amount
@property def balance(self) -> float: return self.__balance
# Usageaccount = BankAccount("Alice", 1000)account.deposit(500) # ✅ Uses the controlled interfaceaccount.withdraw(200) # ✅ Validates before actingprint(account.balance) # ✅ Read-only access via property# account.__balance = 999 # ❌ Can't manipulate directlyWhy it matters: Without encapsulation, any piece of code can modify any data, creating bugs that are impossible to trace. Encapsulation creates trust boundaries—you know that balance can only change through deposit() and withdraw(), both of which validate the operation first.
The most common mistake: Making everything public “for convenience.” If every field is public, you have a struct, not an object. The moment you need to add validation or logging, you’ll have to find and update every place that directly modifies the field.
Deep dive: Encapsulation
2. Abstraction: The Car Dashboard
Pillar 2 Hiding ComplexityThe analogy: When you drive a car, you use the steering wheel, pedals, and gear shift. You don’t need to understand how the engine combustion cycle works, how the transmission converts gear ratios, or how the anti-lock braking system prevents wheel lockup. The car abstracts the complexity behind a simple interface.
In code terms: Abstraction means exposing only what’s necessary and hiding the implementation details. In Python, this is typically done with abstract base classes:
from abc import ABC, abstractmethod
class NotificationSender(ABC): """Users of this class only need to know about send(). They don't care HOW the notification is delivered."""
@abstractmethod def send(self, recipient: str, message: str) -> bool: pass
class EmailSender(NotificationSender): def send(self, recipient: str, message: str) -> bool: # Complex SMTP logic hidden here: # connect to server, authenticate, format MIME, # handle attachments, retry on failure... return self._send_via_smtp(recipient, message)
def _send_via_smtp(self, to: str, msg: str) -> bool: # 50 lines of SMTP details nobody else needs to see ... return True
class SMSSender(NotificationSender): def send(self, recipient: str, message: str) -> bool: # Twilio API integration hidden here return self._send_via_twilio(recipient, message)
def _send_via_twilio(self, to: str, msg: str) -> bool: ... return TrueWhy it matters: Abstraction lets you think at the right level. When designing a notification service, you think about “send a notification”—not “connect to an SMTP server on port 587, authenticate with TLS, format a MIME multipart message…” Abstraction manages the cognitive load of complex systems.
Deep dive: Abstraction
Encapsulation vs Abstraction: The Most Confused Pair
These two get mixed up constantly. Here’s the clearest distinction:
Encapsulation
- Protects internal data from outside access
- Mechanism: Access modifiers (private, protected, public)
- Focus: “Don’t touch my internals”
- Example: Making
__balanceprivate so no one can modify it directly - Level: Implementation detail within a class
Abstraction
- Hides complexity behind a simple interface
- Mechanism: Abstract classes, interfaces, APIs
- Focus: “You don’t need to know how this works”
- Example: A
send()method that hides SMTP vs Twilio internals - Level: Design decision across classes
The one-liner: Encapsulation is about protecting data. Abstraction is about hiding complexity. A class can (and usually should) use both.
3. Inheritance: The Shape Hierarchy
Pillar 3 Code ReuseThe analogy: In biology, a German Shepherd inherits traits from the broader Dog category, which inherits from Mammal, which inherits from Animal. Each level adds specialization while keeping the general traits.
In code terms: Inheritance lets a class (child) derive from another class (parent), inheriting its attributes and methods while adding or overriding its own:
class Shape: def __init__(self, color: str): self.color = color
def area(self) -> float: raise NotImplementedError
def describe(self) -> str: return f"A {self.color} {self.__class__.__name__} with area {self.area():.2f}"
class Circle(Shape): def __init__(self, color: str, radius: float): super().__init__(color) self.radius = radius
def area(self) -> float: return 3.14159 * self.radius ** 2
class Rectangle(Shape): def __init__(self, color: str, width: float, height: float): super().__init__(color) self.width = width self.height = height
def area(self) -> float: return self.width * self.height
# Both work as Shapeshapes: list[Shape] = [Circle("red", 5), Rectangle("blue", 4, 6)]for shape in shapes: print(shape.describe())# "A red Circle with area 78.54"# "A blue Rectangle with area 24.00"Why it matters: Inheritance enables polymorphism (Pillar 4), allows code reuse, and models “is-a” relationships naturally. A Circle is a Shape. A SavingsAccount is a BankAccount.
The danger: Inheritance is the most overused OOP feature. Deep hierarchies (5+ levels) become rigid and hard to change. A common rule of thumb: if you’re inheriting just for code reuse, you’re doing it wrong. That’s what composition is for.
Deep dive: Inheritance
4. Polymorphism: The Universal Remote
Pillar 4 Flexible InterfacesThe analogy: A universal remote has a “power” button that works with your TV, sound bar, and streaming box. The same button, different devices, different behavior. The remote doesn’t need to know how each device turns on—it just sends the signal.
In code terms: Polymorphism means “many forms”—the same interface (method name) behaves differently based on the object’s type:
class PaymentMethod: def pay(self, amount: float) -> str: raise NotImplementedError
class CreditCard(PaymentMethod): def pay(self, amount: float) -> str: return f"Charged ${amount:.2f} to credit card"
class UPI(PaymentMethod): def pay(self, amount: float) -> str: return f"Transferred ${amount:.2f} via UPI"
class Crypto(PaymentMethod): def pay(self, amount: float) -> str: return f"Sent {amount / 50000:.6f} BTC"
def checkout(method: PaymentMethod, amount: float): # This function doesn't know or care which payment type it is print(method.pay(amount))
# All work through the same interfacecheckout(CreditCard(), 99.99) # "Charged $99.99 to credit card"checkout(UPI(), 99.99) # "Transferred $99.99 via UPI"checkout(Crypto(), 99.99) # "Sent 0.002000 BTC"Why it matters: Polymorphism is the foundation of the Strategy pattern—the single most used pattern in LLD interviews. Every time you see “swap algorithms at runtime” or “support multiple types,” you’re using polymorphism.
Python’s superpower—Duck Typing: In Python, you don’t even need inheritance for polymorphism. If an object has a pay() method, it works—regardless of its class hierarchy. “If it walks like a duck and quacks like a duck, it’s a duck.”
class Cash: # No inheritance! def pay(self, amount: float) -> str: return f"Paid ${amount:.2f} in cash"
checkout(Cash(), 99.99) # Works perfectly in PythonDeep dive: Polymorphism
Class Relationships: The Part Most Developers Skip
Knowing the 4 pillars is necessary but not sufficient. The real skill is knowing how classes connect to each other. This is where most LLD designs succeed or fail.
Association: “Uses” or “Knows About”
A Doctor treats a Patient. They interact but exist independently. Neither owns the other.
class Doctor: def __init__(self, name: str): self.name = name
def treat(self, patient: "Patient") -> str: return f"Dr. {self.name} treats {patient.name}"
class Patient: def __init__(self, name: str): self.name = nameThe doctor and patient have their own lifecycles. Deleting the doctor doesn’t delete the patient. This is the loosest relationship.
Deep dive: Association
Aggregation: “Has, But Doesn’t Own”
A Department has Employees, but employees exist even if the department is dissolved. The department doesn’t control their lifecycle.
class Department: def __init__(self, name: str): self.name = name self.employees: list[Employee] = []
def add_employee(self, employee: "Employee"): self.employees.append(employee)
# Employees exist independentlyalice = Employee("Alice")bob = Employee("Bob")engineering = Department("Engineering")engineering.add_employee(alice)engineering.add_employee(bob)# Delete the department — Alice and Bob still existDeep dive: Aggregation
Composition: “Owns and Controls”
A House has Rooms. Destroy the house, and the rooms are gone. The house controls the lifecycle of its rooms.
class House: def __init__(self, address: str): self.address = address # Rooms are created BY the house — they can't exist alone self.rooms = [ Room("Kitchen"), Room("Bedroom"), Room("Bathroom"), ]
class Room: def __init__(self, name: str): self.name = nameThis is the strongest “has-a” relationship and the basis of the Composition over Inheritance principle.
Deep dive: Composition
Dependency: “Temporarily Uses”
A ReportGenerator uses a Printer to print, but doesn’t hold a reference to it. The printer is passed in when needed and then forgotten.
class ReportGenerator: def generate_and_print(self, data: dict, printer: "Printer"): report = self._format(data) printer.print(report) # Uses printer, doesn't own itThis is the weakest relationship—the dependency exists only for the duration of the method call.
Deep dive: Dependency
Inheritance vs Composition: The Debate That Actually Matters
This is the single most important design decision you’ll make in any LLD problem. Get it wrong and your code becomes rigid, fragile, and hard to extend.
When to Use Inheritance
Inheritance is right when there’s a genuine “is-a” relationship and the child class truly represents a specialization of the parent:
# ✅ Good: A SavingsAccount IS a BankAccountclass BankAccount: def deposit(self, amount): ... def withdraw(self, amount): ...
class SavingsAccount(BankAccount): def add_interest(self): ...
# ✅ Good: Each chess piece IS a Piececlass Piece: def can_move(self, board, start, end) -> bool: ...
class Knight(Piece): def can_move(self, board, start, end) -> bool: # L-shaped movement logic ...When to Use Composition
Composition is right when classes have a “has-a” relationship, or when you want to combine behaviors flexibly:
# ✅ Good: A Car HAS an Engine (not "is an" Engine)class Car: def __init__(self, engine: Engine, transmission: Transmission): self.engine = engine self.transmission = transmission
# Now we can mix and match:electric_auto = Car(ElectricEngine(), AutomaticTransmission())gas_manual = Car(GasEngine(), ManualTransmission())hybrid_auto = Car(HybridEngine(), AutomaticTransmission())The Inheritance Trap
Watch what happens when you try to model the same thing with inheritance:
# ❌ Bad: Inheritance explosionclass ElectricAutomaticCar(Car): ...class ElectricManualCar(Car): ...class GasAutomaticCar(Car): ...class GasManualCar(Car): ...class HybridAutomaticCar(Car): ...class HybridManualCar(Car): ...# 3 engines × 2 transmissions = 6 classes. Add 4WD? Now 12 classes.With composition, adding a new engine type means one new class. With inheritance, it means N new classes for every combination.
The Rule of Thumb
Default to composition. Only use inheritance when:
- There’s a clear “is-a” relationship
- The child genuinely specializes the parent’s behavior
- You need polymorphism through a shared base type
If you’re inheriting just to reuse code, composition is almost always better.
Enums: The Unsung Hero of Clean Design
Enums are criminally underused. Every time you see a string like "active", "pending", or "cancelled" hardcoded in your codebase, that’s a place where an enum would prevent bugs.
from enum import Enum
# ❌ Without enums: typos cause silent bugsdef update_order(order_id: int, status: str): ...update_order(123, "actve") # Typo — no error, just wrong data
# ✅ With enums: type safety catches mistakesclass OrderStatus(Enum): PENDING = "pending" CONFIRMED = "confirmed" PREPARING = "preparing" DELIVERED = "delivered" CANCELLED = "cancelled"
def update_order(order_id: int, status: OrderStatus): ...update_order(123, OrderStatus.CONFIRMED) # ✅ Type-safeupdate_order(123, OrderStatus.ACTVE) # ❌ AttributeError — caught immediatelyEnums appear in almost every LLD problem: vehicle types in a parking lot, piece types in chess, machine states in a vending machine, order statuses in a restaurant system.
Deep dive: Enums
UML: The Language of LLD Interviews
You don’t need to be a UML expert, but you need to read and draw basic class diagrams. They’re the communication tool of LLD interviews.
Here’s a quick reference for the notation that matters:
The key symbols:
| Symbol | Relationship | Meaning | Example |
|---|---|---|---|
◁—— (solid line, hollow arrow) | Inheritance | ”is-a” | Dog is an Animal |
◆—— (solid diamond) | Composition | ”owns” (lifecycle dependent) | Dog owns Collar |
◇—— (hollow diamond) | Aggregation | ”has” (independent lifecycle) | Owner has Animals |
- - -> (dashed arrow) | Dependency | ”uses temporarily” | Method parameter |
Visibility markers: + public, - private, # protected
In LLD interviews, you’ll draw these on a whiteboard or describe them verbally. Being fluent in this notation shows you think at the design level, not just the code level.
Deep dive: UML Class Diagrams
Interfaces: Contracts That Keep Code Flexible
Interfaces (or abstract base classes in Python) define what a class must do without specifying how. They’re the backbone of extensible design and appear in every well-designed LLD.
from abc import ABC, abstractmethod
class SearchStrategy(ABC): @abstractmethod def search(self, catalog: list, query: str) -> list: pass
class TitleSearch(SearchStrategy): def search(self, catalog: list, query: str) -> list: return [book for book in catalog if query.lower() in book.title.lower()]
class AuthorSearch(SearchStrategy): def search(self, catalog: list, query: str) -> list: return [book for book in catalog if query.lower() in book.author.lower()]
class ISBNSearch(SearchStrategy): def search(self, catalog: list, query: str) -> list: return [book for book in catalog if book.isbn == query]Why interfaces matter:
- Testability: Mock the interface in unit tests instead of using the real implementation
- Extensibility: Add new strategies without modifying existing code (Open/Closed Principle)
- Decoupling: The caller depends on the interface, not the concrete class (Dependency Inversion)
Deep dive: Interfaces
How OOP Shows Up in LLD Interviews
Let’s connect the theory to practice. Here’s how OOP concepts map to actual interview problems:
| OOP Concept | Interview Application | Example Problem |
|---|---|---|
| Encapsulation | Hiding internal state behind methods | ATM System — balance only changes through validated transactions |
| Abstraction | Abstract base classes for strategies | Rate Limiter — abstract RateLimiter with concrete TokenBucket, SlidingWindow |
| Inheritance | Class hierarchies for entity types | Chess Game — Piece → King, Queen, Rook, etc. |
| Polymorphism | Strategy pattern, interchangeable behaviors | Elevator System — dispatch strategies |
| Composition | Building complex objects from simpler ones | Parking Lot — ParkingLot composed of Floors composed of Spots |
| Enums | State machines, type safety | Vending Machine — MachineState.IDLE, MachineState.DISPENSING |
| Interfaces | Dependency inversion, testability | Notification Service — NotificationSender interface |
The Pattern
Almost every LLD problem follows this structure:
- Identify entities (classes) from requirements
- Define relationships between them (composition, association)
- Use enums for state and types
- Create interfaces for interchangeable behaviors
- Apply polymorphism through the Strategy or State pattern
Master these OOP building blocks and the patterns fall into place naturally.
Common OOP Interview Questions
Beyond design problems, you might get conceptual OOP questions. Here are the ones that come up most:
Abstract class: Can have both abstract (unimplemented) and concrete (implemented) methods. Can have state (instance variables). A class can only extend one abstract class (in most languages).
Interface: Only defines method signatures (no implementation, though Python and Java 8+ allow defaults). A class can implement multiple interfaces.
When to use which:
- Abstract class when you have shared implementation (e.g., a
Vehiclebase class with a commonstart()method) - Interface when you want a pure contract (e.g.,
Searchable,Sortable,Serializable)
In Python, both are implemented using ABC, so the distinction is more about intent than syntax.
When a class inherits from two classes that both inherit from the same base class, creating a diamond shape in the hierarchy. Which version of the base class’s methods does the child get?
Python solves this with the Method Resolution Order (MRO), using the C3 linearization algorithm. Java avoids it by only allowing single class inheritance (but multiple interface implementation).
Practical advice: The diamond problem is a sign that you’re overusing inheritance. Composition avoids it entirely.
SOLID is five design principles that keep your OOP code clean:
- Single Responsibility — one class, one reason to change
- Open/Closed — extend without modifying
- Liskov Substitution — subtypes must be substitutable
- Interface Segregation — small, focused interfaces
- Dependency Inversion — depend on abstractions
These principles prevent the most common OOP design mistakes. We cover them in depth in our SOLID principles guide.
Overriding: A child class provides a different implementation of a method inherited from the parent. Same name, same parameters, different behavior. This is runtime polymorphism.
Overloading: Multiple methods with the same name but different parameter types/counts. This is compile-time polymorphism. Python doesn’t natively support overloading (you use default arguments or *args instead), but Java and C++ do.
Inheritance creates tight coupling — changes to the parent affect all children. It also creates fragile hierarchies that are hard to refactor.
Composition is more flexible: you assemble behavior from independent parts. Want to change the engine? Swap the component. Want to add logging? Wrap with a decorator. No inheritance hierarchy to restructure.
The Gang of Four said it best: “Favor object composition over class inheritance.”
Key Takeaways
Remember These
- Encapsulation protects, abstraction hides — know the difference
- Default to composition over inheritance — it’s more flexible
- Use enums for anything with a fixed set of values — states, types, categories
- Interfaces enable extensibility — they’re the foundation of Strategy, Observer, and Factory patterns
- Learn UML basics — you need to communicate designs visually in interviews
- Class relationships matter as much as classes — association, aggregation, composition, dependency
- OOP is a means, not an end — the goal is clean, maintainable, extensible code
What to Read Next
- What is Low Level Design? — See how OOP concepts become the building blocks of system design
- Top 20 LLD Interview Questions — Practice applying OOP in real interview problems
- Class Relationships Deep Dive — Master association, aggregation, composition, and dependency
- SOLID Principles — The rules that keep OOP designs clean
- Design Patterns — Reusable solutions built on top of OOP concepts
- LLD Playground — Practice 40+ real problems where OOP skills are the difference maker
“The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.” — Joe Armstrong
OOP is a powerful tool, but like any tool, its value comes from knowing when and how to use it. Master the concepts in this guide, and you’ll have the foundation to design clean systems—not just write classes.