The 4 Pillars of OOP
The Mental Model Every Interviewer Expects You to Have
LinkedIn Hook
Most developers can name the 4 pillars of OOP. Almost none of them can explain why each one exists.
Encapsulation. Abstraction. Inheritance. Polymorphism.
If your answer starts with "so basically..." and ends with a vague definition you half-remember, interviewers will move on fast. These four concepts show up in nearly every OOP interview — not as trivia, but as a test of how you actually think about code structure.
In Lesson 1.3, you'll get a clear mental model for each pillar, a real code example for each one, and exactly what to say when the interviewer asks about them.
Read the full lesson → [link]
#OOP #JavaScript #SoftwareEngineering #InterviewPrep
What You'll Learn
- What each of the 4 pillars means — with a single mental model that ties them together
- One runnable JavaScript example per pillar showing the concept in action
- The difference between Encapsulation and Abstraction (the most commonly confused pair)
- How these 4 pillars connect to the rest of the course chapters ahead
The Smartphone Analogy — One Mental Model for All Four
Before writing any code, build a picture in your head. You use a smartphone every day. That phone is a near-perfect demonstration of all four OOP pillars working together.
Think about it this way. The battery, processor, and RAM are locked inside the case — you don't touch them directly. That's Encapsulation. You tap icons on the screen, not circuit boards. The complexity underneath is hidden from you. That's Abstraction. Your phone model inherits features from its base hardware platform. That's Inheritance. The camera app, maps app, and music app all respond to the same "tap to open" gesture, but each does something completely different. That's Polymorphism.
Four concepts. One device you've used a thousand times. Keep this picture in mind as you read each section below.
Pillar 1: Encapsulation — Bundle Data and Protect It
Encapsulation means packaging data (properties) and the methods that operate on that data together inside a single unit — a class. More importantly, it means controlling who can read or change that data from outside.
The goal is to prevent external code from putting an object into an invalid state. Without encapsulation, any part of the codebase could reach in and corrupt an object's data.
The Bank Account Example
A bank account has a balance. Outside code should never be able to write account.balance = -99999 directly. The class needs to control every change through a controlled method.
class BankAccount {
#balance; // private field — no code outside this class can read or write it directly
constructor(initialDeposit) {
if (initialDeposit < 0) {
throw new Error("Initial deposit cannot be negative.");
}
this.#balance = initialDeposit;
}
// Public method — controlled way to change the balance
deposit(amount) {
if (amount <= 0) throw new Error("Deposit amount must be positive.");
this.#balance += amount;
}
// Public method — controlled way to reduce the balance
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient funds.");
this.#balance -= amount;
}
// Public getter — outside code can READ the balance but never write it
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(500);
account.deposit(200);
account.withdraw(100);
console.log(account.getBalance()); // 600
// This throws an error — #balance is private
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
The class bundles the balance and the rules for changing it together. The private field #balance enforces the protection. This is encapsulation in its most direct form.
Pillar 2: Abstraction — Hide Complexity, Show Simplicity
Abstraction means exposing only the relevant details and hiding the internal implementation. Where encapsulation is about protecting data, abstraction is about simplifying the interface.
You don't need to know how a car engine ignites fuel to drive. You just turn the key (or press a button). The interface is simple. The implementation underneath is complex but hidden from you.
The Coffee Machine Example
A coffee machine has pumps, heating elements, pressure controls, and timing circuits. You press one button. The complexity is abstracted away behind a clean interface.
class CoffeeMachine {
// These methods are the internal complexity — not exposed to the user
#heatWater(temperature) {
console.log(`Heating water to ${temperature}°C...`);
}
#grindBeans(grams) {
console.log(`Grinding ${grams}g of beans...`);
}
#buildPressure() {
console.log("Building 9 bars of pressure...");
}
#extractShot(seconds) {
console.log(`Extracting espresso for ${seconds} seconds...`);
}
// Public interface — the user only sees this one method
makeEspresso() {
this.#heatWater(93);
this.#grindBeans(18);
this.#buildPressure();
this.#extractShot(25);
console.log("Espresso ready.");
}
}
const machine = new CoffeeMachine();
machine.makeEspresso();
// Heating water to 93°C...
// Grinding 18g of beans...
// Building 9 bars of pressure...
// Extracting espresso for 25 seconds...
// Espresso ready.
// The user of this class calls ONE method.
// They never touch #heatWater, #grindBeans, etc.
The caller's code says machine.makeEspresso() and nothing else. The entire process is abstracted into a single meaningful action.
Encapsulation vs Abstraction — The Interview Trap
These two are the most commonly confused pair in any OOP interview. Here's the clearest way to separate them:
- Encapsulation is about protection. It restricts direct access to data. The mechanism is private fields and controlled methods.
- Abstraction is about simplification. It hides complexity behind a clean interface. The mechanism is a well-designed public API.
Encapsulation asks: "Who is allowed to touch this data?" Abstraction asks: "What does the user actually need to see?"
Both often appear together. The private methods in the coffee machine example are hidden (encapsulation). The single makeEspresso() method is the simplified interface (abstraction). They work as a team.
Pillar 3: Inheritance — Build on What Already Exists
Inheritance lets a class acquire properties and methods from another class. The class that shares its features is the parent (also called base or superclass). The class that receives them is the child (also called derived or subclass).
The core purpose is code reuse. Instead of rewriting the same logic in every related class, you write it once in a parent and let children inherit it. The child gets all of the parent's behavior, then adds or changes whatever is specific to itself.
The Vehicle Example
Every vehicle has a make, a model, and the ability to start. A car is a vehicle with additional features. A truck is also a vehicle with different additional features.
// Parent class — shared behavior for all vehicles
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
this.isRunning = false;
}
start() {
this.isRunning = true;
console.log(`${this.make} ${this.model} started.`);
}
stop() {
this.isRunning = false;
console.log(`${this.make} ${this.model} stopped.`);
}
}
// Child class — inherits everything from Vehicle, adds its own features
class Car extends Vehicle {
constructor(make, model, doors) {
super(make, model); // call parent constructor first
this.doors = doors;
}
// Method unique to Car
openTrunk() {
console.log(`${this.make} ${this.model} trunk opened.`);
}
}
// Another child class — same parent, different extension
class Truck extends Vehicle {
constructor(make, model, payloadTons) {
super(make, model);
this.payloadTons = payloadTons;
}
// Method unique to Truck
loadCargo(tons) {
if (tons > this.payloadTons) {
console.log("Overloaded!");
} else {
console.log(`Loaded ${tons} tons onto ${this.make} ${this.model}.`);
}
}
}
const car = new Car("Toyota", "Camry", 4);
car.start(); // Toyota Camry started. (inherited from Vehicle)
car.openTrunk(); // Toyota Camry trunk opened. (Car-specific)
const truck = new Truck("Ford", "F-150", 1.5);
truck.start(); // Ford F-150 started. (inherited from Vehicle)
truck.loadCargo(1.2); // Loaded 1.2 tons onto Ford F-150. (Truck-specific)
start() and stop() are written once in Vehicle. Both Car and Truck get them for free. Neither class duplicates that code.
Pillar 4: Polymorphism — One Interface, Many Forms
Polymorphism means "many forms." It lets different classes respond to the same method call in different ways, each appropriate to its own type. You call the same method on different objects, and each object does the right thing for itself.
This is what makes code flexible and extensible. You can write logic that works on a general type, and it will automatically do the right thing for every specific subtype.
The Shape Example
Every shape has an area. But the formula for calculating area is completely different for a circle, a rectangle, and a triangle. Polymorphism lets you call getArea() on any shape without caring which one it is.
class Shape {
// Base method — child classes must override this
getArea() {
throw new Error("getArea() must be implemented by the subclass.");
}
describe() {
// This method works for every shape — it calls getArea() polymorphically
console.log(`Area: ${this.getArea().toFixed(2)}`);
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
// Circle's version of getArea
getArea() {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
// Rectangle's version of getArea
getArea() {
return this.width * this.height;
}
}
class Triangle extends Shape {
constructor(base, height) {
super();
this.base = base;
this.height = height;
}
// Triangle's version of getArea
getArea() {
return 0.5 * this.base * this.height;
}
}
// The power of polymorphism: one loop, three different types, one method call
const shapes = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8),
];
shapes.forEach(shape => shape.describe());
// Area: 78.54
// Area: 24.00
// Area: 12.00
The forEach loop doesn't know or care whether each item is a Circle, Rectangle, or Triangle. It just calls describe() on each. Each object responds with the area formula that belongs to its own type. That's polymorphism.
How the 4 Pillars Connect to the Rest of This Course
Each pillar has its own full chapter ahead. This lesson is the map. Here's where each one goes deeper:
| Pillar | Course Coverage |
|---|---|
| Encapsulation | Chapter 2 — Access modifiers, private fields, getters and setters |
| Abstraction | Chapter 3 — Abstract classes, interfaces, abstraction in JavaScript |
| Inheritance | Chapter 4 — Types of inheritance, super, diamond problem, composition vs inheritance |
| Polymorphism | Chapter 5 — Method overriding, method overloading, overloading vs overriding |
When an interviewer asks about a pillar, they're often testing whether you know the whole concept or just the surface definition. The chapters ahead give you the depth to answer with confidence.
Common Mistakes
-
Confusing Encapsulation with Abstraction. Encapsulation is about protecting data by restricting direct access. Abstraction is about simplifying a complex system behind a clean interface. Encapsulation is the mechanism. Abstraction is the design goal. They often appear together, but they are not the same thing.
-
Thinking Inheritance is always the right tool for code reuse. Inheritance creates a tight coupling between parent and child. If the parent changes, every child is affected. Many experienced developers prefer composition ("has-a") over inheritance ("is-a") for code reuse. Chapter 4 covers this tradeoff in depth.
-
Describing Polymorphism as just "method overriding." Method overriding is the most common mechanism for polymorphism in JavaScript, but polymorphism is the broader concept: the same interface producing different behavior depending on the object's actual type. Overriding is one way to achieve it, not the definition of it.
Interview Questions
Q1: What are the 4 pillars of OOP?
Encapsulation, Abstraction, Inheritance, and Polymorphism. Encapsulation bundles data and methods together and restricts direct access to protect internal state. Abstraction hides implementation complexity behind a simple interface. Inheritance lets a child class acquire and extend the behavior of a parent class. Polymorphism lets different objects respond to the same method call in type-appropriate ways.
Q2: What is the difference between Encapsulation and Abstraction?
(Covered in the Encapsulation vs Abstraction section above.)
Q3: How does Polymorphism make code more flexible and extensible?
Polymorphism lets you write code against a general interface rather than a specific class. When you add a new subtype, the existing code that calls the shared method automatically works with it. In the shapes example, adding a Pentagon class that implements getArea() requires zero changes to the forEach loop that already uses it.
Q4: Why is Inheritance sometimes considered a source of tight coupling?
When a child class inherits from a parent, any change to the parent's interface or behavior can break the child. If you have a deep inheritance chain (grandparent-parent-child-grandchild), a change at the top propagates through every level. This tight coupling makes the system brittle. Composition — where a class holds a reference to another class rather than extending it — gives the same code-reuse benefit with less coupling.
Q5: Can you show all 4 pillars working together in a single real-world example?
A BankAccount class: Encapsulation — the #balance field is private, only changed through deposit() and withdraw(). Abstraction — the caller just calls transfer() without seeing the validation, fee calculation, or logging steps inside. Inheritance — a SavingsAccount extends BankAccount and adds an applyInterest() method. Polymorphism — a processAccounts(accounts) function calls accounts.forEach(a => a.generateStatement()), and each account type (Checking, Savings, Business) produces a different statement format from the same call.
Quick Reference — Cheat Sheet
| Pillar | Core Question | Mechanism in JS | One-Line Definition |
|---|---|---|---|
| Encapsulation | Who can access this data? | #privateField, getters, setters | Bundle data and methods; restrict direct access |
| Abstraction | What does the user need to see? | Private methods, clean public API | Hide complexity; expose only what is necessary |
| Inheritance | How do we reuse behavior? | extends, super | Child class acquires parent's properties and methods |
| Polymorphism | How does one interface produce different behavior? | Method overriding in subclasses | Same method call; different behavior per object type |
+------------------------------------------------------+
| The 4 Pillars at a Glance |
+------------------------------------------------------+
| |
| ENCAPSULATION ABSTRACTION |
| Data + Methods Hide Complexity |
| in one unit Show Simple Interface |
| [Protect] [Simplify] |
| |
| INHERITANCE POLYMORPHISM |
| Child extends Same method, |
| Parent class different behavior |
| [Reuse] [Flexibility] |
| |
+------------------------------------------------------+
Previous: Lesson 1.2 — Class & Object Next: Lesson 1.4 —
thisin OOP Context
This is Lesson 1.3 of the OOP Interview Prep Course — 8 chapters, 41 lessons.