Overview
Encapsulation means hiding an object's internal state and exposing only a controlled set of operations. Other code cannot reach in and set fields to invalid values; it must go through methods that enforce the rules. The object guarantees its own invariants.
Encapsulation is what makes change safe. If an Order stores items in a List today and a Map tomorrow, callers never notice because they only use addItem() and total(). Private fields, returning copies or read-only views instead of internal collections, and small public APIs are the everyday tools.
You control the car with a steering wheel, pedals, and buttons. You cannot directly change the fuel injection timing. The engineers can redesign the engine without changing how you drive.
When to use it
- Any class with rules about its state (balance never negative, status transitions).
- Internal representation may change later.
- Objects shared by many callers who should not break them.
Where it shows up in interviews
Recognize it when: state must stay valid (inventory, balance, status).
- Design a vending machine
- Design a bank account
Recognize it when: getters return internal lists.
- Design an order with line items
- Design a playlist
Where it is used in real software
Collections.unmodifiableList and List.copyOf let classes expose data without allowing external mutation.
Libraries expose small interfaces and keep implementation packages private so they can change internals between versions.
Components hide state and expose it through props and callbacks rather than letting parents mutate it.
Key terms
- Access modifiers
- private, protected, package, public control visibility.
- Invariant
- A condition that must always hold for the object.
- Defensive copy
- Returning or storing a copy so outside code cannot mutate internals.
- Information hiding
- Hide design decisions likely to change.
How it works, step by step
- 1Make fields private
Default to the smallest visibility.
- 2Expose intention-revealing methods
addItem(), cancel(), not setStatus().
- 3Validate in every mutator
Reject invalid transitions.
- 4Never leak mutable internals
Return copies or read-only views.
- 5Keep the public API small
Every public member is a promise.
Leaky vs encapsulated order
An order must never have a negative quantity or be edited after it ships
| Action | Leaky (public list) | Encapsulated |
|---|---|---|
| order.items.push({qty: -3}) | Accepted: corrupt order | Not possible: no direct access |
| order.addItem(sku, -3) | - | Rejected: quantity must be positive |
| Edit after shipping | Anyone can change status | addItem throws: order already shipped |
| Switch List to Map internally | All callers break | No caller changes |
NOWAction: order.items.push({qty: -3}) | Leaky (public list): Accepted: corrupt order | Encapsulated: Not possible: no direct access
Encapsulation turns 'please don't do that' comments into guarantees enforced by code.
Implementation
type Line = { sku: string; qty: number; priceCents: number }; class Order { #lines = new Map<string, Line>(); #status: "open" | "shipped" = "open"; addItem(sku: string, qty: number, priceCents: number) { if (this.#status !== "open") throw new Error("Order already shipped"); if (qty <= 0) throw new Error("Quantity must be positive"); const existing = this.#lines.get(sku); this.#lines.set(sku, { sku, qty: (existing?.qty ?? 0) + qty, priceCents }); } ship() { if (this.#lines.size === 0) throw new Error("Cannot ship an empty order"); this.#status = "shipped"; } lines(): readonly Line[] { return [...this.#lines.values()].map((l) => ({ ...l })); // copies, not internals } total() { return [...this.#lines.values()].reduce((s, l) => s + l.qty * l.priceCents, 0); }}Complexity and performance
Use read-only views for large collections.
Per mutation.
Trade-offs
Strict APIs prevent misuse but require more methods than exposing fields.
Defensive copies cost allocations; immutable structures or views avoid them.
Variants and related techniques
Export only public functions from a module or package.
The strongest encapsulation: state never changes after construction.
Common mistakes
- Getters and setters for every field.
Fix: Setters bypass rules; expose meaningful operations instead.
- Returning the internal list.
Fix: Return an unmodifiable copy or view.
- Validating only in the UI or controller.
Fix: The object must enforce its own invariants.
Interview questions
Why are setters often a sign of weak encapsulation?
A generic setter lets any caller put the object into any state, bypassing business rules. Methods named after actions (ship, cancel) can validate transitions.
How do you expose a collection safely?
Return an unmodifiable copy or view, or expose iteration and query methods, and provide add/remove methods that validate changes.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Encapsulate a Playlist with add, remove, reorder | Easy | Collection safety. |
| Encapsulate a Ticket with status transitions | Medium | Invariants. |