Skip to content
intermediatePhase 49 · Low-Level Design

Encapsulation

Hide internal state and expose only necessary interfaces.

30m
0 problems
Topic Progress0%

Access Modifiers

Access modifiers control the visibility and accessibility of class members. They are the foundation of encapsulation.

Access Levels

┌────────────┬─────────┬─────────┬────────────┬──────────┐
│ Modifier   │ Class   │ Package │ Subclass   │ World    │
├────────────┼─────────┼─────────┼────────────┼──────────┤
│ public     │   ✓     │   ✓     │     ✓      │    ✓     │
│ protected  │   ✓     │   ✓     │     ✓      │    ✗     │
│ (default)  │   ✓     │   ✓     │     ✗      │    ✗     │
│ private    │   ✓     │   ✗     │     ✗      │    ✗     │
└────────────┴─────────┴─────────┴────────────┴──────────┘

Usage Guidelines

public class BankAccount {
    // Private: Internal state (never expose directly)
    private String accountId;
    private double balance;
    private List<Transaction> transactions;
    
    // Protected: Extension points for subclasses
    protected void validateAmount(double amount) {
        if (amount <= 0) throw new IllegalArgumentException();
    }
    
    // Public: API surface (what users of this class can do)
    public void deposit(double amount) {
        validateAmount(amount);
        this.balance += amount;
    }
    
    public double getBalance() {
        return this.balance;
    }
}

The Principle of Least Privilege

Start with the most restrictive access and increase only when needed:

Step 1: Make everything private
Step 2: Add getters for read access
Step 3: Add setters with validation
Step 4: Only use protected for intentional extension
Step 5: Public only for the class API

When to Use Each Modifier

Modifier Use When
private Default for all fields and helper methods
protected Designing for inheritance, framework extension points
default Classes in same package need access
public Part of the class's external API

Common Mistakes

  1. Public fields: No validation, no encapsulation
  2. Overly permissive: Everything public "for convenience"
  3. Anemic models: Only getters/setters, no behavior
  4. Leaking internals: Returning mutable internal collections

Getters and Setters

Getters and setters provide controlled access to private fields. They are not just accessors — they are opportunities for validation and logic.

Basic Getter/Setter

public class User {
    private String name;
    private int age;
    private String email;
    
    // Getter
    public String getName() {
        return this.name;
    }
    
    // Setter with validation
    public void setName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this.name = name.trim();
    }
    
    public int getAge() {
        return this.age;
    }
    
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
        this.age = age;
    }
}

Computed Properties

class Rectangle {
    private double width;
    private double height;
    
    // Computed property (no field backing)
    public double getArea() {
        return width * height;
    }
    
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

Read-Only Properties

public class Order {
    private final String id;
    private final LocalDateTime createdAt;
    private OrderStatus status;
    
    // Read-only: no setter for id and createdAt
    public String getId() { return id; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    
    // Mutable with controlled setter
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) {
        // Business logic: validate transitions
        if (!this.status.canTransitionTo(status)) {
            throw new IllegalStateException("Invalid status transition");
        }
        this.status = status;
    }
}

Defensive Copying

public class ShoppingCart {
    private List<Item> items = new ArrayList<>();
    
    // Bad: Returns reference to internal list
    public List<Item> getItemsBad() {
        return items;  // Caller can modify!
    }
    
    // Good: Returns defensive copy
    public List<Item> getItems() {
        return new ArrayList<>(items);
    }
    
    // Good: Accepts defensive copy
    public void setItems(List<Item> newItems) {
        this.items = new ArrayList<>(newItems);
    }
}

When to Skip Getters/Setters

Situation Approach
Value objects Only getters, no setters (immutable)
Internal state Private with no accessors
Derived values Only getter (computed)
Write-only Only setter (e.g., password hash)

Data Hiding

Data hiding goes beyond access modifiers. It's about concealing implementation details so that classes can change internally without affecting consumers.

What to Hide

Public API (What users see)     Internal Implementation
─────────────────────────      ──────────────────────
User user = new User();         - Database schema
user.setName("John");          - Cache strategy
String name = user.getName();   - Serialization format
                                 - Validation rules
                                 - Internal state machines

Information Hiding Patterns

Pattern 1: Hide Data Source

public class UserRepository {
    public User findById(String id) {
        // Caller doesn't know if data comes from
        // database, cache, or API
        return cache.get(id);
    }
}

Pattern 2: Hide Algorithm

public class PricingService {
    public Money calculatePrice(Order order) {
        // Caller doesn't know the pricing algorithm
        Money base = calculateBasePrice(order);
        Money discount = calculateDiscount(order);
        Money tax = calculateTax(base.subtract(discount));
        return base.subtract(discount).add(tax);
    }
}

Pattern 3: Hide State Transitions

public class Order {
    private OrderStatus status;
    
    public void pay() {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException();
        }
        this.status = OrderStatus.PAID;
    }
    
    public void ship() {
        if (status != OrderStatus.PAID) {
            throw new IllegalStateException();
        }
        this.status = OrderStatus.SHIPPED;
    }
    // Callers don't need to know the state machine
}

Benefits of Data Hiding

  1. Change freedom: Modify internals without breaking consumers
  2. Validation: Ensure invariants are maintained
  3. Security: Prevent unauthorized access
  4. Simplification: Reduce cognitive load for users
  5. Flexibility: Add logging, caching, or transactions transparently

Encapsulation in System Design

┌─────────────────────────────────────────────┐
│              Payment Service                 │
├─────────────────────────────────────────────┤
│ Public: processPayment(order), refund(order)│
├─────────────────────────────────────────────┤
│ Internal (hidden):                           │
│  - Fraud detection algorithm                 │
│  - Payment provider routing                  │
│  - Retry logic                               │
│  - Transaction management                    │
│  - idempotency key generation                │
└─────────────────────────────────────────────┘

Users only see: processPayment() → success/failure
They don't see: fraud check → provider selection → retry → ...

Practice Problems

0/3solved
Design Encapsulation System

Design a scalable Encapsulation system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
Encapsulation Scaling

How would you scale Encapsulation to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
Encapsulation Failure Modes

Analyze potential failure modes for Encapsulation and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. Which access modifier allows access within the same package but not to subclasses outside the package?

Question 1 options

2. Why should you return defensive copies of mutable internal collections?

Question 2 options

3. What is the principle of least privilege in access control?

Question 3 options

4. Which is NOT a benefit of data hiding?

Question 4 options

5. When should you NOT provide a setter for a field?

Question 5 options

Flashcards

Question

What is encapsulation?

Answer

Bundling data and methods that operate on that data into a single unit (class), while restricting direct access to some components to protect object integrity.

Question

What are the 4 access modifiers in Java?

Answer

public (everywhere), protected (same package + subclasses), default/package (same package only), private (same class only).

Question

What is defensive copying?

Answer

Creating copies of mutable objects when returning them from getters or accepting them in setters, to prevent external modification of internal state.

Question

What should setters validate?

Answer

Null values, range boundaries, format validity, and business rule compliance. Setters enforce class invariants.

Question

What is the difference between encapsulation and data hiding?

Answer

Encapsulation is bundling data + methods. Data hiding is restricting access to internal details. Encapsulation is the mechanism; data hiding is the outcome.

Revision Notes

Key Takeaways

  • 1.Encapsulation bundles data and methods while restricting access
  • 2.Start with private and increase visibility only when needed
  • 3.Setters provide validation opportunities to maintain invariants
  • 4.Defensive copying prevents external modification of internal state
  • 5.Data hiding enables internal changes without breaking consumers

Interview Tips

  • Explain why you're using specific access levels in your design
  • Show how setters validate data to maintain object invariants
  • Discuss how data hiding allows internal implementation changes
  • Mention defensive copying when returning mutable objects

Cheat Sheet

Encapsulation - Cheat Sheet

Access Modifiers:

Modifier Class Package Subclass World
public
protected
default
private

Getter/Setter Rules:

  1. Validate in setters (null, range, format)
  2. Return defensive copies of mutable collections
  3. Skip setters for immutable fields
  4. Computed properties: only getter

Data Hiding Benefits:

  • Internal changes don't break consumers
  • Invariants maintained through validation
  • Security via restricted access
  • Reduced coupling

Principle of Least Privilege:
Start private → increase only when needed