Encapsulation
In simple words
Encapsulation means keeping an object's data private, and letting other code reach it only through methods. Think of a medicine capsule: the ingredients are sealed inside, and you swallow the capsule instead of opening it and mixing the chemicals yourself. In code, outside code can't set balance = -5000 directly. It has to call something like debit(5000), and that method checks the rules first and refuses if the balance is too low.
The core move — hide state, expose behaviour
Without encapsulation With encapsulation
───────────────────── ────────────────────────────
account.balance = -5000 account.debit(5000)
// the method validates first,
// throws if balance is insufficient
The field becomes unreachable from outside. Every change goes through a method that can validate, log, or reject.
Access modifiers — how you enforce it
| Modifier | Reachable from |
|---|---|
private | only inside the same class |
protected | same class + subclasses |
public | everywhere |
| (default / package-private) | same package |
Rule of thumb:
- Fields → always
private. - Internal helper methods →
private. - Methods that form the class's public API →
public. - Methods only subclasses need →
protected.
Bad vs good
Without encapsulation — broken
class BankAccount {
public double balance; // anyone can set this
public String pin; // exposed — a security hole
}
BankAccount acc = new BankAccount();
acc.balance = -99999; // invalid state, nothing stops it
acc.pin = ""; // empty PIN, nobody checks
With encapsulation — correct
class BankAccount {
private double balance; // hidden
private String pin; // hidden
BankAccount(double initialBalance, String pin) {
if (initialBalance < 0) throw new IllegalArgumentException("Balance cannot be negative");
if (pin == null || pin.length() < 4) throw new IllegalArgumentException("Invalid PIN");
this.balance = initialBalance;
this.pin = pin;
}
void debit(double amount, String enteredPin) {
if (!this.pin.equals(enteredPin)) throw new SecurityException("Wrong PIN");
if (amount > this.balance) throw new IllegalStateException("Insufficient balance");
this.balance -= amount;
}
void credit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
this.balance += amount;
}
}
What changed: balance and pin are unreachable from outside; every operation validates first; the object cannot be put into an invalid state by a caller.
Getters and setters are NOT automatic
Generating a getter and setter for every field just moves the exposed field one method away — it isn't encapsulation.
// NOT encapsulation — a raw pass-through with no rules
class User {
private String password;
public String getPassword() { return password; }
public void setPassword(String p) { password = p; } // no validation, no hashing
}
// Encapsulation — the class owns the rules
class User {
private String passwordHash;
void setPassword(String raw) {
if (raw.length() < 8) throw new IllegalArgumentException("Too short");
this.passwordHash = BCrypt.hash(raw); // validate + hash inside
}
boolean verifyPassword(String raw) {
return BCrypt.verify(raw, this.passwordHash);
}
// no getPassword() — no caller ever needs the raw value
}
Before writing a getter, ask: does the caller actually need to see this value? Often the answer is no — they need the object to do something with it.
Also: don't hand out your internal collection.
// leaks — the caller can now mutate your list
List<Item> getItems() { return this.items; }
// safe — a read-only view
List<Item> getItems() { return Collections.unmodifiableList(this.items); }
"Tell, don't ask"
Encapsulation pushes logic into the object that owns the data instead of pulling data out to compute elsewhere.
// asking — logic leaks into the caller
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}
// telling — the object enforces its own rule
account.debit(amount);
If the "can I debit?" rule lives in the caller, it gets copy-pasted into every caller and they drift out of sync. Inside debit() there is exactly one copy.
At system-design scale
A microservice encapsulates its database.
OrderService UserService users table
──────────── ─────────────── ───────────
needs an email ──► getEmail(userId) ──► SELECT email ...
◄── returns email ◄──
OrderServicenever queries the users table directly.- All access to user data goes through
UserService's methods. UserServicecan change its schema, add a cache, or add validation — nobody else notices.
"No service reads another service's database" is encapsulation applied to a distributed system. Break it and you're back to public fields — every schema change can break a service you didn't know existed.
How This Connects
- Abstraction (previous topic) — the sibling. Encapsulation hides data; abstraction hides logic/implementation. Both put up a boundary; interviewers check you can tell them apart.
- Classes Objects & Instances — the private-field / public-method split is why a Spring singleton service must be stateless: shared mutable state that isn't properly guarded is a race condition.
- Cohesion & Coupling — encapsulation lowers coupling: callers depend on the method contract, not the field layout, so internals are safe to refactor.
- SOLID Principles — encapsulation is how you keep a class's single responsibility actually contained.
- Module 3 — HLD / Module 10 — Security — "each service owns its own data store" and "validate at the boundary" are encapsulation at architecture scale.
Trade-offs & common mistakes
- Don't make a field
public"just for now" — every caller that touches it becomes a thing you must not break later. - Don't auto-generate getters/setters for every field. Expose behaviour, not state.
- Don't return your internal
List/Map/array directly — return a copy or an unmodifiable view. - Do put validation and business rules inside the class that owns the data, so there's one copy.
- Note the cost: over-guarding tiny value objects (a
Pointwith x/y) with ceremony adds noise. Immutable value objects with public final fields (or records) are fine — there's no invalid state to protect.
In the Interview
When to say it. Any time you talk about keeping something valid or keeping services independent:
- "the account object validates the debit itself, so it can never go negative"
- "each service owns its schema — others go through its API"
- "the password class hashes on the way in; there's no getter for the raw value"
The ~20-second version:
"Encapsulation is hiding an object's internal state and only exposing behaviour through methods that enforce the rules. A
BankAccountkeepsbalanceprivate — you can't set it directly; you calldebit(), which validates before changing state, so the object can never be in a broken condition. At system scale, a microservice encapsulates its database: other services call its API, never its tables. That gives you one place to enforce rules and the freedom to change internals without breaking callers."
Follow-ups to expect:
- "Isn't a getter + setter the same thing?" — No. A raw pass-through setter with no validation just exposes the field through a method. Encapsulation means the class owns the rules (validate, hash, reject).
- "How is it different from abstraction?" — Encapsulation hides data/state; abstraction hides implementation/logic.
private balancevs aPaymentGatewayinterface. - "What breaks if you skip it?" — Objects reach invalid states, validation logic gets copy-pasted into every caller and drifts, and you can't refactor internals because callers depend on the exact field layout.
Mistake to avoid: exposing an internal collection by reference. The caller can then mutate your object's state without going through any of your methods — silent, hard-to-trace bugs.
Quick Revision — 6 lines
- Encapsulation =
privatefields +publicmethods that guard them. Outside code calls a method; the method enforces the rules. - It makes invalid state impossible from outside — validation lives inside the class.
- Getters/setters aren't automatic. Ask "does the caller need this value?" — usually they need the object to do something instead.
- Never return your internal collection by reference — return a copy or an unmodifiable view.
- "Tell, don't ask" — push the logic into the object that owns the data, so there's one copy of the rule.
- At system scale: each microservice owns its data store; others go through its API. Break that and every schema change is a landmine.