Beginner11 min readObject-Oriented Foundationslive prototype

Encapsulation

Hide an object's internal state behind methods so it protects its own rules — outside code can't reach in and break it.

The idea

What it is

Think of a medicine capsule. The powder inside is sealed — you don't pour it out and re-measure it yourself. You take the capsule as a whole, and the casing makes sure you get exactly the right dose. Encapsulation is that idea in code: an object wraps its data inside a protective shell and only lets you interact with it through a few trusted methods.

Without it, any code anywhere could reach into an object and scramble its data — set a bank balance to -9999, give an order a quantity of zero, put a date in an impossible state. With encapsulation, the object becomes the guardian of its own data. Want to change something? You go through a method, and that method gets to say yes or no.

The one sentence to remember

Make the data private, expose methods that guard it. The object protects its own rules so no outside code can put it in a broken state.

Mechanics

How it works

Private vs. public

Every member of a class is either public — anyone can see and touch it — or private — only the object's own methods can. Encapsulation is the habit of keeping data private and making a small, deliberate set of methods public. The public methods are the object's front door; the private fields are the locked room behind it.

Picture a vending machine. There's a locked cash box inside (private). You can't open it and take the money. What you can do is press buttons and insert coins (public methods). The machine decides whether to dispense a snack and how much change to return. The rules live with the machine, not with whoever walks up to it.

Access modifiers

Languages give you keywords to mark this boundary. In TypeScript it's private (or a #name field that's truly hidden at runtime). In Java and C++ it's private. Python has no hard private, so the convention is a leading underscore like _balance, often paired with a property to control access. Same idea everywhere: a label that says this is internal, don't touch it from outside.

Getters and setters — done right

A getter reads a private field; a setter changes it. The point of routing through them is not to mindlessly mirror the field — it's to add a checkpoint. A good setter validates:

  • Validate before you write. A deposit(amount) rejects a negative amount; a setTemperature(c) rejects anything below absolute zero. The method refuses bad input instead of storing it.
  • Expose read-only when that's all you need. Often you want a getter and no setter at all — outsiders can look at the balance but can only change it by depositing or withdrawing.
  • Don't just pass the field through. A setter that does this._balance = value with no check gives away everything privacy bought you. If a field has no rules, maybe it doesn't need a setter.

Invariants: the rules the object guards

An invariant is a fact that must always be true about an object — for a bank account, balance >= 0. Encapsulation is what lets the object promise that invariant. Because the only way in is through deposit and withdraw, and those methods check the rule, the balance can never go negative. The object enforces its own correctness.

account.ts
class BankAccount {
  // private — the front door is the methods below, not this field
  #balance = 0;

  deposit(amount: number) {
    if (amount <= 0) throw new Error("amount must be positive");
    this.#balance += amount;            // guarded write
  }

  withdraw(amount: number) {
    if (amount > this.#balance) throw new Error("insufficient funds");
    this.#balance -= amount;            // invariant balance >= 0 holds
  }

  get balance() { return this.#balance; } // read-only view, no setter
}

const a = new BankAccount();
a.deposit(100);
// a.#balance = -5  ← won't even compile; the field is unreachable

Why data and its guard methods belong together

The validation rules and the data they protect are two halves of one thing. If they're split apart — data in one place, the checks scattered across the codebase — sooner or later someone changes the data without running the checks. Keeping balance and the deposit/withdraw that guard it in the same class means there's exactly one trusted path in, and the rules can't be skipped.

Encapsulation vs. abstraction

They sound alike but solve different problems. Encapsulation hides and protects data (private fields behind guard methods). Abstraction hides complexity (a simple interface over messy internals). Encapsulation is about who can touch what; abstraction is about what you have to know.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

One BankAccount drawn as a capsule with a shielded interior holding the private balance. Try the left column — account.balance = X — and the shield flashes red: outside code can't touch a private field. Use the right column — deposit and withdraw — and watch validation run before the balance changes. The balance >= 0 invariant never breaks.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Try to break in directly

Type a number into the left column and press account.balance = X. The shield flashes red and the log says the field is private — outside code can't touch it. The balance doesn't budge. That's the privacy boundary doing its job.

try 02

Go through the methods

Use the right column to deposit(50), then withdraw(30). Valid operations flash green and the shielded balance updates. This is the front door — the only sanctioned way to change the data.

try 03

Watch validation reject bad input

Try deposit(-20) or withdraw more than the current balance. The method refuses and logs why, and the balance >= 0 invariant badge stays satisfied. The object guards its own rules even when you ask it to misbehave.

In practice

When to use it — and what trips people up

When to make things private

  • When a field has rules — a balance that can't go negative, a percentage that must stay 0–100, a status that only changes in a set order.
  • When you want to change the internals later without breaking callers — hide the field now, and you're free to swap how it's stored.
  • When data and the logic that protects it belong together — keep them in one class so the checks can't be skipped.

Make fields private by default

Start every field as private and only open it up when you have a concrete reason. It's far easier to expose something later than to claw back access once code all over the codebase depends on a public field.

What it gives you

  • The object guards its own invariants — bad states become impossible, not just discouraged.
  • One trusted path in means validation can't be bypassed or forgotten.
  • You can change the internal representation freely without breaking any caller.
  • Bugs are easier to find — if the balance is wrong, the cause is in one small set of methods.

Common mistakes

  • Anemic getters/setters that just expose every field — that's a public field wearing a disguise, with none of the protection.
  • Leaking mutable internals — returning the actual list or array lets callers mutate your private state behind your back.
  • Over-encapsulating — wrapping a plain value object with no rules in layers of ceremony adds noise for nothing.
  • Treating private as security — it stops accidents and enforces design, but it isn't a defense against a determined attacker.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// balance is private; the only way to change it is through guarded methods.
class BankAccount {
  #balance: number;

  constructor(opening = 0) {
    if (opening < 0) throw new Error("opening balance can't be negative");
    this.#balance = opening;
  }

  deposit(amount: number) {
    if (amount <= 0) throw new Error("deposit must be positive");
    this.#balance += amount;
  }

  withdraw(amount: number) {
    if (amount <= 0) throw new Error("withdraw must be positive");
    if (amount > this.#balance) throw new Error("insufficient funds");
    this.#balance -= amount;
  }

  // read-only view — no setter, so outsiders can look but not poke
  get balance() {
    return this.#balance;
  }
}

const acct = new BankAccount(100);
acct.deposit(50);     // → 150
acct.withdraw(30);    // → 120
// acct.#balance = -5 ← syntax error: '#balance' is not accessible
// acct.balance  = -5 ← error: no setter; 'balance' is read-only

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Nothing is scored or saved.

question 01 / 04

What is the core purpose of encapsulation?

question 02 / 04

A BankAccount has a private balance and a withdraw(amount) method that rejects amounts greater than the balance. Why keep balance private?

question 03 / 04

Which getter/setter pair actually adds value over a plain public field?

question 04 / 04

How do encapsulation and abstraction differ?

0/4 answered