What is encapsulation in software design, and why is making every field private while adding a public getter and setter for each one not really encapsulation?
answer
- Behavior, not slots
- Setter per field = layout published
- Invariants need atomic transitions
- Tell, don't ask
- DTOs are a fair exception
basics
~20 sEncapsulation means a component keeps its internal data private and exposes only meaningful operations. A getter and setter for every field still lets outsiders read and change all the state, so nothing is actually hidden and no rules are enforced.
solid answer
~50 sEncapsulation bundles data with the operations that maintain it and restricts direct access, so the component can guarantee its own correctness (its invariants) and change its internals without breaking callers. The public surface should describe what the object does, not what it stores. Field-per-accessor classes fail both goals. Correctness: any caller can write a value that breaks an invariant (a negative balance, an end date before the start date), so validation ends up copy-pasted across call sites instead of living in one place. Change: the accessors mirror the fields one-to-one, so renaming, splitting, or re-representing a field breaks everyone. The public API has become a published copy of the private layout. Real encapsulation replaces setters with intention-revealing operations — deposit(amount), reschedule(newRange) — that validate input, keep related fields consistent, and hide how state is actually stored. Read-only exposure of derived values is fine; unrestricted mutation is what leaks.
code
pseudocode · 14 lines// Leaky: layout is the contract, invariant unenforceable
class Booking { private start, end
getStart(); setStart(d); getEnd(); setEnd(d) }
booking.setEnd(yesterday) // now invalid, nobody stopped it
// Encapsulated: one atomic, validating operation
class Booking {
private start, end
reschedule(newStart, newEnd) {
require(newStart <= newEnd)
this.start = newStart; this.end = newEnd
}
durationDays() = daysBetween(start, end)
}go deeper
Define encapsulation as hiding internal data behind meaningful operations; give one concrete invariant a setter would break.
Add the change-cost argument: accessors mirroring fields publish the layout, so you can never re-represent state. Mention immutability and constructor validation.
Distinguish encapsulation (mechanism), abstraction (model choice), and information hiding (what to conceal). Discuss atomic multi-field transitions and where DTOs legitimately opt out.
Frame it as coupling economics — the accessor set is the true contract and its size sets your future change cost. Talk about enforcing the boundary with tooling and about the same failure at module/service scale, not just class scale.
## Definitions first - **State**: the data a component holds internally (fields/attributes/member variables). - **Public interface**: the names callers are allowed to use — methods, functions, exported types. Everything else is an *implementation detail*. - **Invariant**: something that must be true about the state before and after every public operation. Example: `start <= end`; `balance >= 0`; `items` never contains null. - **Encapsulation**: keeping state and the operations over it together, and restricting access so the operations are the only way the state changes. ## Why "private field + get/set" is a costume, not encapsulation Consider a date range with private `start` and `end`, plus `getStart/setStart/getEnd/setEnd`. Two things go wrong. **1. Invariants cannot be enforced.** `setEnd(yesterday)` on a range starting tomorrow produces an invalid object. Even if each setter validates in isolation, it cannot validate a *transition* that requires changing two fields at once: swapping a range from Jan–Feb to Mar–Apr requires two calls, and after the first call the object is temporarily invalid. Any code reading it in between (another thread, a logger, a serializer, a callback) sees a broken object. A single operation — `moveTo(newStart, newEnd)` — validates the whole transition atomically. **2. The public API is glued to the layout.** Because there is one accessor per field, the field list *is* the contract. Want to store a `start + duration` instead of two dates? Want to move `end` to a nested value object? Every caller breaks. The whole point of hiding internals is the freedom to change them; a mirrored accessor set gives that freedom away. ## What to do instead - Expose **behavior**, not slots: `withdraw(amount)` rather than `setBalance(...)`. This is often summarized as **Tell, Don't Ask** — tell the object to do something rather than asking for its data and computing outside. - Validate in the **constructor/factory** so an object is never born invalid, and in each mutating operation so it never becomes invalid. - Prefer **immutability** where practical: no setters at all; operations return a new instance. Invariants then only need checking once, at construction. - If callers genuinely need to read a value, a **getter alone** is fine — it is the unrestricted *write* path and the one-to-one *shape* mirroring that do the damage. Prefer returning derived, meaningful values (`isOverdue()`, `durationDays()`) over raw internals. ## Legitimate exceptions Some types are honestly just data: DTOs at a serialization boundary, configuration records, database row mappings, event payloads. They have no invariants worth defending and their whole job is to carry fields across a wire. Giving those public fields or generated accessors is correct, not a violation — the mistake is treating *domain* objects, which do have rules, the same way. Frameworks that require a no-arg constructor plus setters push people into this habit; isolate such types at the edge and keep the rule-bearing model behind them. ## Encapsulation vs. abstraction vs. information hiding They are related but distinct. *Abstraction* is choosing a simplified model (a `Queue` concept). *Information hiding* (Parnas) is deciding which design decisions to conceal because they are likely to change. *Encapsulation* is the mechanism — language access control and API shape — that makes the hiding hold. You can encapsulate badly-chosen abstractions, and you can have a great abstraction leak because encapsulation is weak.
- If a class exposes a getter that returns its internal list, is that encapsulated?No. Returning the live collection lets callers add/remove elements behind the object's back, bypassing every validation. Return an unmodifiable view or a copy, or better, expose operations (add/remove/count) that enforce the rules.
- Aren't records/data classes an encapsulation violation then?Not inherently. They are the right tool for values with no invariants beyond construction-time validation — especially immutable ones, where public read access is harmless because nothing can be mutated. The anti-pattern is a mutable, setter-driven object that owns real business rules.
A vending machine has a coin slot and buttons, not a door to the cash box. Getters and setters for everything is a machine with a hinged front panel: technically 'closed', but anyone can rearrange the stock and take the money, and you can never change the internal racking without breaking someone's habits.