<arch.design/>
Principles/Encapsulation
{ }CodeArchitecturebeginner1967oopaccess-controlinformation-hidinginvariants

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/5
{ }
Operates at: Code level

Inside 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 · Rust
class 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 field

Why 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

Java / Kotlin

private fields + public getters/setters is idiomatic — IDEs generate them automatically

Rust

Fields are private by default — pub must be explicit; the module system enforces encapsulation at compile time

Go

Unexported (lowercase) fields are package-private — the standard library uses this pervasively (sync.Mutex, http.Client)

Industry adoption

5/5Ubiquitous — used at virtually every scale-focused company.

Related principles