Skip to main content

Encapsulation and Information Hiding as Design Goals

What This Concept Is​

Encapsulation establishes a boundary around state and operations. Information hiding chooses which design decisions that boundary keeps clients from depending on. Access modifiers can help implement the boundary; they do not establish the design property alone.

In most introductions, encapsulation collapses to "use private fields and write getters." That misses the point. The real question is: which design decisions are likely to change, and how can they change without the rest of the code caring?

Information hiding (Parnas's term) is the clearer name. A module hides what it knows and exposes what it can do. Good modules export small, stable operations and keep volatile decisions invisible.

Why It Matters Here​

Bad encapsulation is the quiet cause of most expensive-to-change codebases. When every client knows the shape of a data structure, the ordering of its fields, the meaning of its null values, or the identity of its backing store, that is what you cannot change without a trail of edits.

Most refactorings in later modules (Encapsulate Record, Encapsulate Collection, Encapsulate Variable, Replace Primitive with Object) exist to buy back encapsulation that was lost by treating an object as a bag of fields.

Concrete Example​

Leaky encapsulation -- private field, public getter, same thing:

class Account {
private List<Transaction> transactions = new ArrayList<>();
public List<Transaction> getTransactions() { return transactions; }
}

// elsewhere
account.getTransactions().add(new Transaction(...));
account.getTransactions().sort(...);

The field is private, but the list is exposed by identity. Callers can mutate and sort the exposed list. The declared List type does not promise ArrayList, but aliasing already lets clients change internal structure.

Real encapsulation -- Account hides the representation and the invariants:

class Account {
private final List<Transaction> transactions = new ArrayList<>();

public void record(Transaction t) {
validate(t);
transactions.add(t);
}

public Money balance() { return Money.sum(transactions); }
public List<Transaction> history() { return List.copyOf(transactions); }
}

The returned list cannot be structurally modified through its API, and later additions to the original list do not appear in it. This is a shallow snapshot: mutable Transaction elements may still be shared. Make the records immutable or define copying/ownership before claiming stable history. List.copyOf also rejects null elements.

Common Confusion / Misconception​

"My fields are private, so the class is encapsulated." Privacy of syntax is not privacy of decision. If every field has a getter and setter and your clients rebuild all invariants outside, you have just renamed struct to class.

A second confusion: encapsulation is not about secrecy. It is about stability. You are choosing which parts of the design can change quietly. Anything you expose, you are promising to keep stable; anything you hide, you are free to rework.

How To Use It​

Before finishing a class, run three sweeps:

  1. Decision sweep. For each design decision (data layout, algorithm, storage backend, error style), ask: is this exposed? If yes, commit to it as a contract; if no, hide it.
  2. Return-value sweep. Specify snapshot versus live view, element mutability, ordering, ownership and cost. An unmodifiable live view and a shallow snapshot make different promises; neither automatically deep-copies elements.
  3. Behavior sweep. Are callers composing multiple calls to enforce invariants (get / validate / set)? Pull the composition into the class as one method.

Check Yourself​

  1. Why is "private fields with getters and setters" often still unencapsulated?
  2. What is the difference between encapsulation and security?
  3. Why is information hiding closer to the real goal than "data hiding"?

Mini Drill or Application​

Pick a class with at least three fields that are exposed through getters. Do all four:

  1. For each getter, ask what the caller does with the result.
  2. Decide whether each transformation is domain behavior or a separate presentation concern before moving it.
  3. Choose a snapshot, live view or owned mutable result deliberately and test aliases, including nested elements.
  4. Rewrite the class's public contract in one paragraph: "this class lets callers X, Y, and Z, and hides A, B, and C."

Read This Only If Stuck​

Engineering extension: Snapshots require an ownership story​

Imagine a list containing a mutable transaction with amount 10. A new unmodifiable outer list can still refer to that same transaction; another alias changes amount to 20 and both lists now observe 20. For stable history, use immutable elements or a deliberately deep snapshot. The offline account lab uses a tuple of integers, so both the outer structure and the exposed elements resist ordinary mutation. It does not claim protection from hostile reflection. Try: distinguish an old snapshot from a new query after a deposit.

Use the design contracts and practice feedback after an independent attempt.