System Design
1. System Prerequisites

Encapsulation

⏱ 0s active on this topic

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

ModifierReachable from
privateonly inside the same class
protectedsame class + subclasses
publiceverywhere
(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      ◄──
  • OrderService never queries the users table directly.
  • All access to user data goes through UserService's methods.
  • UserService can 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 Point with 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 BankAccount keeps balance private — you can't set it directly; you call debit(), 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:

  1. "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).
  2. "How is it different from abstraction?" — Encapsulation hides data/state; abstraction hides implementation/logic. private balance vs a PaymentGateway interface.
  3. "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

  1. Encapsulation = private fields + public methods that guard them. Outside code calls a method; the method enforces the rules.
  2. It makes invalid state impossible from outside — validation lives inside the class.
  3. Getters/setters aren't automatic. Ask "does the caller need this value?" — usually they need the object to do something instead.
  4. Never return your internal collection by reference — return a copy or an unmodifiable view.
  5. "Tell, don't ask" — push the logic into the object that owns the data, so there's one copy of the rule.
  6. At system scale: each microservice owns its data store; others go through its API. Break that and every schema change is a landmine.

Quick-revision quiz

Question 1 of 17
A class has `private double balance;` plus `public getBalance()` and `public setBalance(double b)` with no checks in the setter. Is this encapsulation?