Encapsulation

How encapsulation protects object state, enforces invariants, and makes code maintainable — the OOP principle every senior developer relies on.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Encapsulation means hiding the internal state of an object and only exposing a controlled interface. You make fields private and provide public methods (getters/setters) that control how the data is accessed and modified. This lets you enforce rules — like "salary cannot be negative" — that would be impossible if fields were public.

🔍 What Is It?

Encapsulation is one of the four pillars of OOP. It means:

1. Data Hiding

Fields are declared private — they cannot be accessed directly from outside the class.

2. Controlled Access

Public getter and setter methods provide controlled read/write access.

3. Invariant Enforcement

Setters validate data before accepting it, ensuring the object is always in a valid state.

4. Implementation Independence

Internal representation can change without affecting callers — only the public interface matters.

Access Modifiers:

  • →private — accessible only within the same class
  • →package-private (no modifier) — accessible within the same package
  • →protected — accessible within same package + subclasses
  • →public — accessible from anywhere

🎯 Why Do We Need It?

Without encapsulation:

// BAD: public fields, no protection
class BankAccount {
    public double balance;  // anyone can set balance = -999999
}
account.balance = -1000000;  // silently broken!

With encapsulation:

// GOOD: private field, controlled access
class BankAccount {
    private double balance;
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        this.balance += amount;
    }
}

Encapsulation enforces business rules at the object level, making it impossible to put an object in an invalid state.

🏗️ What Happens in Real Projects?

In enterprise projects, encapsulation appears everywhere:

  • →JPA Entities — private fields with getters; setters only where needed
  • →Validation — setters throw IllegalArgumentException for invalid data
  • →Immutable Value Objects — no setters at all; state set only in constructor
  • →Builder Pattern — encapsulates complex construction logic
  • →Service Layer — business rules encapsulated in service methods, not exposed directly

⚙️ How It Works

Getter/Setter Convention:

  • →Getter: public Type getFieldName() (or isFieldName() for boolean)
  • →Setter: public void setFieldName(Type value)

Lombok reduces boilerplate:

@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class Employee { ... }

Immutability is the strongest form of encapsulation:

No setters + final fields = object cannot change after construction = thread-safe by default.

💻 Example

Encapsulation in a Banking Domain — A BankAccount class showing how encapsulation enforces business rules.

java
public class BankAccount {
    private final String accountNumber;  // immutable
    private final String ownerId;        // immutable
    private BigDecimal balance;
    private boolean frozen;
    private final List<String> transactionLog = new ArrayList<>();
    
    public BankAccount(String accountNumber, String ownerId) {
        this.accountNumber = Objects.requireNonNull(accountNumber);
        this.ownerId = Objects.requireNonNull(ownerId);
        this.balance = BigDecimal.ZERO;
        this.frozen = false;
    }
    
    // Business method — encapsulates deposit logic
    public void deposit(BigDecimal amount) {
        validateNotFrozen();
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        this.balance = this.balance.add(amount);
        log("DEPOSIT", amount);
    }
    
    // Business method — encapsulates withdrawal logic
    public void withdraw(BigDecimal amount) {
        validateNotFrozen();
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Withdrawal amount must be positive");
        }
        if (amount.compareTo(this.balance) > 0) {
            throw new InsufficientFundsException(
                "Insufficient funds. Balance: " + balance + ", Requested: " + amount
            );
        }
        this.balance = this.balance.subtract(amount);
        log("WITHDRAWAL", amount);
    }
    
    // Read-only access to balance
    public BigDecimal getBalance() { return balance; }
    
    // Read-only access to transaction log (defensive copy)
    public List<String> getTransactionLog() {
        return Collections.unmodifiableList(transactionLog);
    }
    
    // No setter for balance — balance can only change via deposit/withdraw
    // No setter for accountNumber or ownerId — they are immutable
    
    public void freeze() { this.frozen = true; }
    public void unfreeze() { this.frozen = false; }
    public boolean isFrozen() { return frozen; }
    
    private void validateNotFrozen() {
        if (frozen) throw new IllegalStateException("Account is frozen");
    }
    
    private void log(String type, BigDecimal amount) {
        transactionLog.add(LocalDateTime.now() + " " + type + ": " + amount);
    }
    
    // Getters for immutable fields
    public String getAccountNumber() { return accountNumber; }
    public String getOwnerId() { return ownerId; }
}

🤔 Common Confusions

❓ If I have getters and setters for every field, am I really encapsulating?

Not really. A class with public getters and setters for every field is barely better than public fields — it is "anemic encapsulation." True encapsulation means only exposing what callers need, and enforcing invariants in setters. Ask: does every field need a setter? Can some operations be expressed as business methods (deposit, withdraw) instead of raw setters?

❓ What is the difference between encapsulation and information hiding?

They are closely related but not identical. Encapsulation is the mechanism (private fields + public methods). Information hiding is the principle (callers should not need to know implementation details). Encapsulation is how you achieve information hiding in Java.

❓ Should I always use getters/setters or can fields ever be public?

There are valid cases for public fields: records (Java 16+, though they use accessors), simple data holders in test code, and mathematical types like Point where x and y are inherently public. In production business code, keep fields private. The Java community convention is clear: private fields with accessors.

🚨 Common Production Issues

Returning mutable collections from getters — if getList() returns the internal list, callers can modify it. Return Collections.unmodifiableList() or a copy.

Setters that bypass validation — adding a setter to fix a compilation error without thinking about invariants breaks encapsulation silently.

Anemic domain model — entities with only getters/setters and no business logic. Business rules end up scattered in service classes, making the domain model meaningless.

✅ Best Practices

Make all fields private by default. Add getters only when needed, setters only when mutation is required.

Return defensive copies or unmodifiable views of mutable collections from getters.

Prefer immutability — no setters, final fields, records. Immutable objects are thread-safe by default.

Express business operations as methods (deposit, activate, cancel) rather than raw setters.

Use Lombok @Value or records for immutable data classes to reduce boilerplate.

🏛️ Architect's Note

Senior Architect's Perspective

The debate between rich domain models (business logic in entities) and anemic domain models (logic in services) is ongoing. Rich models follow true encapsulation — the Order class knows how to add items, calculate totals, and validate itself. Anemic models put all logic in OrderService. For complex domains, rich models are more maintainable. For simple CRUD apps, anemic models are fine. Choose based on domain complexity.

💬 Frequently Asked Questions

What is a defensive copy and when do I need one?

A defensive copy is a copy of a mutable object made to prevent external modification. If your class stores a Date or List passed in the constructor, a caller could modify the original after construction. Solution: store a copy — this.date = new Date(date.getTime()). Similarly, return copies from getters. With Java 8+ Instant (immutable) and records, defensive copies are less needed.

How does encapsulation relate to thread safety?

Immutable objects (all final fields, no setters) are inherently thread-safe — they cannot be modified after construction, so no synchronization is needed. Mutable encapsulated objects still need synchronization if accessed from multiple threads. Encapsulation makes it easier to add synchronization in one place (the setter/method) rather than everywhere callers access the field.

🎯 Key Takeaways

1Make fields private; provide public methods for controlled access
2Setters should validate data and enforce business invariants
3Returning mutable collections from getters breaks encapsulation — return unmodifiable views
4Immutability is the strongest form of encapsulation — and is thread-safe by default
5Express business operations as methods, not just raw getters/setters
6Lombok @Value or records eliminate boilerplate for immutable classes

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide Core Java learning path

Browse the full Core Java topic collection and learning order on SplashRide.

Core Java Interview Prep

Practice Core Java interview questions and scenario-based answers.

Backend Engineer Career Path

A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.

Full Stack Java Developer Career Path

A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.

Classes & Objects

The foundation of Java OOP — how classes define blueprints and objects are instances, with constructors, fields, methods, and the this keyword.

Inheritance

How Java inheritance works — extends, super, method overriding, and when to use inheritance vs composition in real enterprise projects.