Week 2 Day 2 — Encapsulation and Access
Goal
Today I want encapsulation as hiding how the object works, not as “add getters.”
Main questions:
- What is encapsulation?
- What do
private, package,protected, andpublicmean? - Why are public fields a problem?
- When are getters justified?
- How does this show up in Spring services and entities?
1. Encapsulation
Encapsulation means the object owns its invariants. Callers use a small public API. Fields stay hidden so I can change representation without breaking callers, and so invalid states are hard to construct.
public final class Quantity {
private final int value;
public Quantity(int value) {
if (value <= 0) {
throw new IllegalArgumentException("quantity");
}
this.value = value;
}
public Quantity plus(int n) {
return new Quantity(this.value + n);
}
public int value() {
return value;
}
}
The invariant “quantity is positive” lives in one place. A public int quantity field cannot protect that.
Memory sentence:
Encapsulation keeps the invariant inside the object. Accessors are optional; protection is not.
2. Access modifiers
From most hidden to most open:
| Modifier | Visible to |
|---|---|
private | The class itself |
| (package) no modifier | Classes in the same package |
protected | Same package and subclasses in other packages |
public | Everyone |
There is no package keyword. Omitting the modifier is package access.
Practical defaults I use:
- fields:
private - constructors used by Spring or factories:
public(or package for tests in the same package) - service methods that are use cases:
public - helpers:
private - types I want as API:
public; types I want as implementation: package-private
protected is for planned inheritance. If I do not have a subclass story, I do not use it.
3. Getters are not encapsulation
This is not encapsulated:
public class Order {
public List<Line> lines;
}
This is only slightly better:
public class Order {
private List<Line> lines;
public List<Line> getLines() { return lines; }
public void setLines(List<Line> lines) { this.lines = lines; }
}
Callers can still clear the list, add negative quantities, or set lines to null. I published a mutable bag.
Better:
public class Order {
private final List<Line> lines = new ArrayList<>();
public void add(Line line) {
Objects.requireNonNull(line);
lines.add(line);
}
public List<Line> lines() {
return Collections.unmodifiableList(lines);
}
}
The object exposes operations, not its storage.
For DTOs at the HTTP boundary, a record with components is honest: there is no invariant beyond “this is the payload.” Do not pretend a DTO is a rich domain object.
4. Packages as a boundary
com.example.orders.api vs com.example.orders.internal is a design tool.
- Public types in
apiare what other packages may use. - Package-private types in
internalare implementation. - Spring component scanning still needs public (or at least instantiable) types for beans it creates, but other application code should depend on interfaces, not on the concrete class, when I want a seam.
Week 3 will make that seam an interface. Encapsulation is the reason the interface is small.
5. Spring connection
Services: fields private final, no setters, public use-case methods. I do not expose the repository.
Controllers: public mapped methods. They should be thin: validate, call the service, map to a response.
Entities: JPA often needs a no-arg constructor and access to fields. I still keep fields private and avoid publishing internal collections. Framework access (field injection by Hibernate) is not the same as public API.
Field injection in Spring:
@Autowired
private OrderRepository orders; // works, and is weakly encapsulated
The field is private, but the object can be created without its collaborator, and I cannot make the field final. Constructor injection keeps encapsulation and invariants: after new (or after Spring constructs), the object is complete.
Memory sentence:
Private fields plus a constructor invariant beat private fields plus
@Autowired.
6. Common traps
Trap 1: “I have getters, so I have OOP.”
Getters can leak the same mutable state.
Trap 2: protected fields “for Spring.”
Spring does not need that. Subclasses then depend on storage.
Trap 3: Public DTO fields as a domain model.
Fine as a JSON bag. Not fine as the type that enforces business rules.
Trap 4: Package-private tests that reach into fields with reflection as a habit.
If the test needs the field, the production API is probably wrong, or I should inject a fake through the constructor.
Practice Questions and Answers
Question 1
What is encapsulation?
Answer:
The object hides its representation and exposes a small API that preserves its invariants. Callers cannot put it into an invalid state without going through that API.
Question 2
Why are public fields a problem on a domain type?
Answer:
Any caller can write any value at any time. Validation in the constructor is pointless if the field can change later. I also cannot change the representation without breaking every caller.
Question 3
Does a getter encapsulate a List field?
Answer:
Not if it returns the live list. Callers mutate it. Return an unmodifiable view, a copy, or offer add/remove methods instead.
Question 4
Why does constructor injection fit encapsulation better than field injection?
Answer:
The collaborator is required to construct the object, the field can be final, and tests pass a fake without reflection. Field injection creates a half-built object and hides the dependency.
Question 5
When is a public record with all components acceptable?
Answer:
When the type is a value or DTO: the data is the API, and there is little invariant beyond validation on construction. That is different from a service or an aggregate that must protect a collection of lines.
Memory sentences
Encapsulation keeps the invariant inside the object.
A getter that returns a live list is a public field with extra typing.
Construct a complete object. Do not poke
privatefields from the container if a constructor will do.