Module 20 · OOP fundamentals · Lesson 2 of 5
Encapsulation
Hiding state behind behaviour so an object can protect its own rules. Access modifiers across languages, why getters and setters are not encapsulation, leaking mutable internals and defensive copies, and the 'tell, don't ask' habit.
Why it matters
If any code anywhere can change an object's fields, the object cannot keep its own rules, and every bug that breaks them could have come from anywhere. Encapsulation shrinks that blast radius to one class. It is also what lets you change an implementation (a list becomes a map, a field becomes computed) without touching callers. Of the four pillars, it is the one with the most direct effect on how easy code is to change safely.
The idea
Encapsulation means an object's state is accessible only through its own methods, so the object alone decides how that state may change. It has two halves: information hiding (callers cannot see how the object stores things) and bundling (the behaviour that maintains the state lives with the state).
Access modifiers
| Level | Java | C# | TypeScript | Python |
|---|---|---|---|---|
| Anyone | public | public | public (default) | name |
| Subclasses | protected (also same package) | protected | protected | convention only |
| Same module or package | package-private (no keyword) | internal | not available; use module exports | _name by convention |
| This class only | private | private | private or #name (runtime-enforced) | __name (name mangling, not real privacy) |
Default to the most restrictive level that works. Every public member is a promise you will have to keep; every private one is a detail you are free to change.
Getters and setters are not encapsulation
A class with private fields and a public getter and setter for each one has the syntax of encapsulation and none of the benefit: any caller can still set any value, and the rules still live in the callers. Encapsulation is about exposing operations that mean something in the domain and enforce rules, not about wrapping fields.
// Not encapsulated: rules live in every caller
order.setStatus(Status.SHIPPED);
order.setShippedAt(Instant.now());
// Encapsulated: one method, one place for the rule
order.ship(clock); // throws if the order is not paid; sets status and timestamp together
This is the tell, don't ask habit: tell an object what to do, rather than asking for its data and deciding for it.
Leaking mutable internals
Returning a reference to an internal mutable collection hands callers the keys. They can add, remove or clear items without going through any of your checks. Return an unmodifiable view or a copy, and copy mutable arguments on the way in if you store them.
public List<Item> items() { return items; } // leaks: caller can mutate
public List<Item> items() { return List.copyOf(items); } // safe snapshot
public List<Item> items() { return Collections.unmodifiableList(items); } // safe live view
Immutability: the strongest form
An object whose state never changes after construction cannot be put into a bad state later by anyone. Value-like objects (money, dates, coordinates, identifiers) should usually be immutable: final fields, no setters, operations that return new objects. Immutable objects are also safe to share between threads without locks.
Trade-offs
Strict encapsulation costs some convenience: frameworks that map objects to databases or JSON often want setters or field access, and tests sometimes want to peek inside. The usual compromise is encapsulated domain objects at the core and simple data objects at the edges, mapped explicitly. Over-encapsulating trivial data holders adds ceremony without protecting any rule, so reserve the discipline for objects that have rules.
In an interview
Show it through method names: "Seat exposes hold, release and confirm rather than a setStatus; confirm checks the hold belongs to the same user and has not expired." When a design returns a collection, mention returning an immutable copy. Both signal that the class owns its rules.
Check yourself
A Team class keeps a private List of members and enforces a maximum of 10 in addMember. It also has a method getMembers() that returns the private list directly. What is the problem?