Skip to content

Encapsulation

Encapsulation is the practice of bundling data (properties) and methods (behavior) that operate on that data within a single unit (class/object), while restricting direct access to the internal state.

It’s one of the four fundamental pillars of OOP, often called data hiding.

  • To protect data from accidental or unauthorized modification
  • To enforce invariants — ensuring objects are always in a valid state
  • To create a clear interface between an object and the outside world
  • To reduce complexity by hiding implementation details
  • Prevents bugs caused by direct field manipulation
  • Makes code more maintainable (change internals without affecting callers)
  • Provides controlled access through getters and setters
  • Enables validation and computation during access

When you use an ATM:

  • You can’t directly access the bank’s vault (encapsulated data)
  • You can only use the interface (insert card, enter PIN, press buttons)
  • The ATM validates your actions (sufficient balance? correct PIN?)
  • You never touch the actual money storage mechanism

This is encapsulation: the internal state (cash, account balances) is hidden, and all operations go through a controlled interface.


Encapsulation is the mechanism of wrapping data (variables) and code (methods) together as a single unit, and restricting direct access to some of an object’s components.


Without encapsulation:

// ❌ No encapsulation — anyone can change anything
let balance = 1000;
// Direct modification — no validation!
balance = -500; // Negative balance! Bug!

With encapsulation:

// ✅ Encapsulated — controlled access
class BankAccount {
#balance = 0;
constructor(initial) {
if (initial >= 0) this.#balance = initial;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
// account.#balance = -500; // ❌ SyntaxError: Private field
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300

graph LR
subgraph Outside["Outside World"]
User[User Code]
end
subgraph Object["BankAccount Object"]
Interface["Public Interface<br/>deposit() withdraw() getBalance()"]
Private["Private State<br/>#balance"]
Validation["Validation Logic<br/>amount > 0<br/>amount <= balance"]
end
User -->|"deposit(500)"| Interface
Interface -->|Validate| Validation
Validation -->|Update| Private
User -->|"getBalance()"| Interface
Interface -->|Read| Private
style Private fill:#4CAF50,color:#fff
style Interface fill:#2196F3,color:#fff

class Employee {
// Private fields (using # syntax)
#salary;
#ssn;
constructor(name, salary, ssn) {
this.name = name; // Public
this.#salary = salary; // Private
this.#ssn = ssn; // Private
}
// Public getter
getSalary() {
return this.#salary;
}
// Public setter with validation
setSalary(amount) {
if (amount >= 0) {
this.#salary = amount;
} else {
throw new Error("Salary cannot be negative");
}
}
// Private method
#validateSSN() {
return /^\d{3}-\d{2}-\d{4}$/.test(this.#ssn);
}
}
const emp = new Employee("Alice", 75000, "123-45-6789");
console.log(emp.name); // "Alice"
// console.log(emp.#salary); // ❌ SyntaxError
console.log(emp.getSalary()); // 75000
emp.setSalary(80000);
BenefitDescription
Data ProtectionPrevents unauthorized or accidental modification
ValidationControl what values are assigned to properties
FlexibilityChange internal implementation without affecting users
MaintainabilityLocalize changes within the class
TestabilityTest the interface, mock the internals
ReadabilityClear separation of public API and private details

  • Default to private — Make fields private unless there’s a good reason to expose them
  • Use getters/setters for controlled access to properties
  • Validate in setters — Ensure data integrity before assignment
  • Don’t expose internal references — Return copies or immutable wrappers
  • Keep public API minimal — Less surface area means less to maintain

  • Making everything public — Exposing internal state leads to tight coupling
  • Returning mutable references to internal collections
  • Exposing too many getters — If every field has a getter, you’ve created a data structure, not an object
  • Getters and setters for everything — Don’t add them until needed
  • Encapsulating incorrectly — Putting related data in different objects

Easy:

  1. What is encapsulation in OOP?
  2. How does encapsulation improve code security?
  3. What’s the difference between public and private members?

Medium: 4. How is encapsulation implemented in JavaScript (different methods)? 5. What’s the relationship between encapsulation and abstraction? 6. How do getters and setters support encapsulation?

Advanced: 7. How does encapsulation relate to the Single Responsibility Principle? 8. What’s the difference between data hiding and encapsulation? 9. How do you handle encapsulation in functional programming?


ConceptKey Point
EncapsulationBundling data + methods, hiding internals
PurposeProtect data, enforce valid state
ImplementationPrivate fields + public methods
Access ControlPrivate → Protected → Public
BenefitsSecurity, flexibility, maintainability
Rule of ThumbMake fields private; expose only what’s needed

Previous Topic: Four Pillars → Next Topic: Abstraction → Related Topics: Access Modifiers, SOLID