Architecture & LLD

Encapsulation

OOPS PRINCIPLE 2

Encapsulation

Let's start with something concrete. Imagine a BankAccount. In the real world, a bank account isn't just a number written on paper β€” it's a thing that has information and can do things. OOP lets us model it exactly that way.

πŸ“¦
State
What the object knows about itself
owner
= "Alice"
// who owns the account
balance
= $4,200
// how much money is in it
isActive
= true
// is the account open?
⚑
Behaviour
What the object can do
deposit(amount)
add money to the balance
withdraw(amount)
remove money from the balance
getBalance()
check how much is in it
A natural question

The account has a balance field. That balance needs to be readable β€” you need to know your balance. But does any code anywhere in the system have the right to directly change it? What if someone writes account.balance = -9999 by accident? There's nothing stopping it. That's the problem Encapsulation solves.

01

The Problem β€” Unprotected State

If balance is a public field, any code anywhere can corrupt it
account.balance = -9999
Negative balance
Nothing stops this. Account is instantly broken.
account.balance = 99999999
No upper limit
No daily cap check β€” silently invalid amount.
account.balance = NaN
Not a number
Future calculations crash β€” but you see nothing now.
account.balance = 0
Accidental wipe
A one-character typo resets the whole balance.
A method is a gatekeeper β€” it validates the input before touching the field

Instead of letting code set balance directly, you provide a deposit(amount) method. Inside that method, you write if-conditions before the field is ever modified. If the check fails, the method prints a message and returns early β€” balance is never touched.

Check 1
Is amount a valid number?
βœ“ proceed to check 2
βœ— reject β€” stop here
Check 2
Is it greater than zero?
βœ“ proceed to check 3
βœ— reject β€” no negatives
Check 3
Is it within daily limit?
βœ“ balance += amount
βœ— reject β€” cap exceeded

To force everything through the method, mark the field private. Now account.balance = -9999 won't even compile. The method is the only door in. That is Encapsulation. But to enforce that, you need the language's help. That's what access modifiers are for.

Access Modifiers

Think of your bank as a building with different rooms. Not every room is open to everyone β€” who's allowed in depends on which door they're standing at.

In code, every field and method you write is one of those rooms. An access modifier is the keyword that decides who gets a key.

πŸ›οΈ
publicβ€” The Lobby
Anyone can walk in β€” customers, staff, strangers, anyone.

The lobby has no guards. In code, a public field or method can be accessed from anywhere β€” another class, an external library, anywhere in your codebase.

// from anywhere in the codebase β€” no check, no guard
account.balance = -9999  // βœ“ compiles. Nothing stops it.

Use public for methods that others are meant to call β€” like deposit() or getBalance(). Never for raw mutable fields β€” that's handing strangers the keys to the vault.

πŸ”’
privateβ€” The Vault
Only the bank itself can open this. Not employees. Not managers. Nobody else.

The vault is sealed from the outside. In code, a private field can only be read or changed by code inside the same class. Not by subclasses. Not by other classes. Just this one.

// outside the class β€” the compiler stops you
account.balance = -9999  // βœ— compile error β€” access denied

// inside BankAccount β€” the only place allowed
this.balance += amount  // βœ“ methods inside can touch it

This is the default you should reach for with every field. Make it private. Expose it through a method. Write the validation once inside that method. Done.

πŸ”
protectedβ€” The Staff Room
Staff only β€” and staff from branches of the same chain.

The staff room is off-limits to customers β€” but a SavingsAccount branch of the same bank can get in too. In code, protected means the class itself and any subclass that extends it can access the field. You'll hit this most when using inheritance.

class SavingsAccount extends BankAccount {
  void addInterest() {
    this.balance *= 1.05;  // βœ“ child class β€” allowed
  }
}

Don't default to protected β€” use it only when a child class genuinely needs direct field access that a method can't cleanly provide.

Rule of Thumb

Start with private for everything. Only upgrade to protected when a subclass genuinely needs it, or public when something is intentionally part of the external interface. The tighter the access, the less that can go wrong.

02

Try It β€” Broken Bank vs Safe Bank

Both accounts start at $500. On the left, balance is public β€” type anything and it is accepted without question. On the right, balance is private β€” every input goes through the validation checks above. Try -999, 0, abc, 200000 on both sides.

BROKEN β€” No Encapsulation
// balance is a public field
account.balance = ??? // anything goes
Balance right now:
$500
Type a value and click Set…
SAFE β€” with Encapsulation
// balance is private β€” use methods
account.deposit(amt) / withdraw(amt)
Balance right now:
$500
Try the same bad values here…
03

The Pattern in Code

private locks the field. Public methods are the only doors. Validation lives inside each method β€” written once, always enforced.

BankAccount.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class BankAccount {
// private = only accessible within this class
private balance: number = 0;
// The ONLY public way to add money
deposit(amount: number): void {
if (amount <= 0) {
console.log("Rejected: must be positive");
return;
}
if (amount > 100000) {
console.log("Rejected: daily limit exceeded");
return;
}
this.balance += amount;
}
// The ONLY public way to remove money
withdraw(amount: number): void {
if (amount <= 0 || amount > this.balance) {
console.log("Rejected: invalid withdrawal");
return;
}
this.balance -= amount;
}
// Read-only getter
getBalance(): number {
return this.balance;
}
}
UTF-8Spaces: 4TypeScript
πŸ›‘οΈ

The One-Line Takeaway

Make fields private. Make methods the only door. Write validation inside once β€” it's always enforced.

Up next: Abstraction