Encapsulation
Encapsulation hides internal representation and exposes a controlled interface.
Introduction
Encapsulation hides internal representation and exposes a controlled interface. Callers depend on what an object does, not how it stores data — the foundation of maintainable LLD.
In interviews, broken encapsulation shows up as public collections mutated from anywhere, or balance fields updated without validation. Reviewers infer production bugs: double spending, invalid state transitions, race conditions.
Strong encapsulation pairs with small public APIs — often the difference between a extensible design and a fragile one.
Understanding the topic
Key concepts
- Hide fields; expose behavior through methods.
- Invariant protection: only the owning class changes critical state.
- Defensive copies when returning mutable internals (lists, dates).
- Package-private for collaboration within a module; public for stable API.
- Encapsulation enables refactoring internal structure without client changes.
- Thread-safe encapsulation: synchronize or confine mutations to one thread.
Step-by-step explanation
- Make fields private (or record components already immutable).
- Route state changes through methods that validate preconditions.
- Return unmodifiable views of collections (List.copyOf, Collections.unmodifiableList).
- Avoid leaking this during construction if partially initialized.
- Use builder pattern only when construction complexity warrants it.
- Document which methods are thread-safe.
Informative example
Bank account with encapsulated balance — no direct field access:
public final class BankAccount {private final String accountId;private long balanceCents;public BankAccount(String accountId, long openingBalanceCents) {this.accountId = Objects.requireNonNull(accountId);if (openingBalanceCents < 0) throw new IllegalArgumentException("negative opening balance");this.balanceCents = openingBalanceCents;}public synchronized void deposit(long cents) {if (cents <= 0) throw new IllegalArgumentException("deposit must be positive");balanceCents += cents;}public synchronized boolean withdraw(long cents) {if (cents <= 0 || cents > balanceCents) return false;balanceCents -= cents;return true;}public synchronized long balanceCents() { return balanceCents; }}
synchronized methods encapsulate concurrency policy inside the account — callers cannot forget to lock.
Real-world use
Real-world applications
- Protecting invariants in ATM, wallet, and inventory classes.
- Publishing stable module APIs in large codebases.
- Preventing unauthorized state jumps in state machines.
Best practices
- Never expose mutable collections directly.
- Validate at system boundaries (constructors, command handlers).
- Prefer immutability for value types — encapsulation by default.
- Keep getters minimal; favor intention-revealing methods (withdraw vs setBalance).
- Use records for transparent immutable values when no behavior needed.
- Log state transitions inside the class, not from outside.
Common mistakes
- public List
seats — external code removes elements illegally. - Setter-only models with no validation.
- Returning internal array references that callers mutate.
- Breaking encapsulation with instanceof chains across packages.
Advanced interview questions
Q1BeginnerWhat is encapsulation?
Q2BeginnerWhy not make all fields public?
Q3IntermediateHow do you encapsulate a collection?
Q4IntermediateDoes encapsulation conflict with testing?
Q5AdvancedHow would you encapsulate multi-field updates atomically?
Summary
Hide data; expose controlled behavior. Validate and synchronize inside the class boundary. Defensive copies prevent external mutation. Small public APIs reduce coupling. Encapsulation enables safe refactoring.