Encapsulation
Bundle an object's data and the methods that operate on it into a single unit, and hide internal state behind a public interface.
★★★★★5/5Inside a codebase — classes, modules, files
How it works
Encapsulation is one of the four pillars of object-oriented programming, first introduced in Simula (1967). It has two parts: bundling (keeping related data and behaviour in one place) and access control (deciding what the outside world can see and change).
In practice, fields are marked private or unexported, and the class exposes only what callers need through a deliberately designed public API. Callers interact with the interface — they never directly read or write internal state.
This boundary lets you change the internal implementation (swap a list for a hash map, add validation, change the storage format) without breaking any caller. It also prevents invalid state: a BankAccount that exposes its balance field can have it set to -∞ from anywhere; one that hides it behind deposit() and withdraw() can enforce invariants in a single place.
Implementation
TypeScript · Go · Rustclass BankAccount {
#balance = 0;
deposit(amount: number): void {
if (amount <= 0) throw new RangeError("Amount must be positive");
this.#balance += amount;
}
withdraw(amount: number): void {
if (amount > this.#balance) throw new RangeError("Insufficient funds");
this.#balance -= amount;
}
get balance(): number {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
account.withdraw(30);
console.log(account.balance); // 70
// account.#balance = 9999; // ❌ SyntaxError — private fieldWhy it matters
When internal state is publicly writable, every caller becomes a potential source of corruption. Encapsulation concentrates validation and invariant enforcement in one place — the class itself — making bugs easier to find and fix.
✓ When to use
- →Any object that has state that must stay internally consistent
- →When you want to change the internal representation without breaking callers
- →When validation or business rules govern how state can change
- →Effectively always — encapsulation is not optional in well-designed OO code
✗ When NOT to use
- →Plain data transfer objects (DTOs) with no behaviour can expose public fields
- →Value objects with all-immutable fields need no access control
Trade-offs
Internal representation can change without breaking callers
Extra boilerplate — getters/setters for every field can feel verbose
Invariants are enforced in one place
Anemic models (data bags with no methods) suggest encapsulation was applied incorrectly
Reduces surface area for bugs — fewer ways to produce invalid state
Over-encapsulation hides necessary information and forces awkward workarounds
In production
private fields + public getters/setters is idiomatic — IDEs generate them automatically
Fields are private by default — pub must be explicit; the module system enforces encapsulation at compile time
Unexported (lowercase) fields are package-private — the standard library uses this pervasively (sync.Mutex, http.Client)
Industry adoption
Related principles
Abstraction
Reduce complexity by modelling only the details that matter for your problem — hide everything else behind a stable interface.
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Single Responsibility Principle
A class should have only one reason to change — keep each unit focused on a single job so unrelated concerns don't accidentally break each other.
Composition over Inheritance
Build complex behaviour by combining small, focused objects rather than extending a class hierarchy — keeping coupling low and flexibility high.