skip to content

Encapsulation and Information Hiding

Hide what is likely to change behind a stable interface so callers cannot depend on your internals or break your invariants. This is Parnas information hiding, and interviewers probe it whenever they ask why a field should not simply be public.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

5

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?

level: juniorimportance: must knowfreq 88%

answer

  1. Behavior, not slots
  2. Setter per field = layout published
  3. Invariants need atomic transitions
  4. Tell, don't ask
  5. DTOs are a fair exception

basics

~20 s

Encapsulation 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 s

Encapsulation 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
pseudocode
// 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

for a junior

Define encapsulation as hiding internal data behind meaningful operations; give one concrete invariant a setter would break.

for a middle

Add the change-cost argument: accessors mirroring fields publish the layout, so you can never re-represent state. Mention immutability and constructor validation.

for a senior

Distinguish encapsulation (mechanism), abstraction (model choice), and information hiding (what to conceal). Discuss atomic multi-field transitions and where DTOs legitimately opt out.

for a principal

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.

context

open as a page

David Parnas argued that modules should be decomposed by information hiding rather than by processing steps. What does that mean in practice, and how do you decide what each module hides?

level: middleimportance: must knowfreq 62%

basics

~20 s

Instead of splitting a system into one module per step of the process, split it so each module hides one design decision that is likely to change — a data format, an algorithm, a device, a policy. Everything that would change together lives behind one interface.

open as a page

What is representation exposure, and how does returning an internal mutable collection or storing a caller-supplied object break a class's invariants?

level: middleimportance: must knowfreq 58%

basics

~20 s

Representation exposure is handing out a reference to your internal mutable data. If a method returns the live list, or the constructor stores the caller's list directly, the caller can change that data afterwards without going through your validation, so your rules stop holding.

open as a page

How do you decide which parts of a component belong in its stable public interface and which must stay volatile implementation details? What signals tell you a detail has leaked?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Publish what callers need to express their intent and what you can promise to keep working; hide anything you expect to change — data layout, algorithms, libraries, protocols. A detail has leaked when a change to it forces callers to change too.

open as a page

Encapsulation at the class level is enforced by access modifiers. What enforces it at the module or service level, and why is a shared database between two services the classic failure of encapsulation at that scale?

level: principalimportance: should knowfreq 45%

basics

~20 s

At module or service scale there is no private keyword across process boundaries, so encapsulation must be enforced by explicit published interfaces plus tooling and ownership. A shared database breaks it because every table is effectively a public field any service can read and write.

open as a page