Java Encapsulation Capstone: 4 Production-Grade Secure Systems

โ˜• Java 21+ LTS ๐ŸŸข Chapter 47 of 47 ๐Ÿ“‚ Phase 10: Encapsulation & Access Modifiers ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter:

Project 1: Secure Bank Account (Core Snippet) ยท Project 2: Student Grade Book ยท Project 3: Product Inventory with Access Control ยท Project 4: Configuration Manager

Building 4 complete production-grade systems applying full encapsulation principles: secure access control, validated mutation, immutable identifiers, computed properties, and protected internal state โ€” culminating in a Configuration Manager with package-level isolation design.

1. Encapsulation Design Checklist

Before writing any class in production Java, verify this design checklist:

โœ… All instance fields declared private
  โœ… Immutable identity fields declared private final (no setter)
  โœ… All setters contain appropriate business validation
  โœ… Getters for boolean fields use "is" prefix
  โœ… Mutable collection/array fields returned as unmodifiable or cloned
  โœ… Business logic encapsulated in methods (not leaked to callers)
  โœ… toString() overridden for meaningful logging and debugging
  โœ… Access modifiers chosen following the Principle of Least Privilege

Beginner Example & Code Anatomy

โ˜• Main.java โ€” Chapter 47 Core Example
import java.util.*;

// =====================================================================
// PROJECT 1: SECURE BANK ACCOUNT (User's Core Snippet โ€” Fully Expanded)
// =====================================================================
class BankAccount {
    private static int accountSerial = 1000;

    private final String accountNumber;
    private final String holderName;
    private double balance;
    private final List<String> transactionLog;

    public BankAccount(String holderName, double initialDeposit) {
        if (holderName == null || holderName.isBlank())
            throw new IllegalArgumentException("Holder name required.");
        this.holderName    = holderName.trim();
        this.accountNumber = "ACC-" + (++accountSerial);
        this.balance       = Math.max(0, initialDeposit);
        this.transactionLog = new ArrayList<>();
        logTxn("ACCOUNT OPENED", initialDeposit);
    }

    // Read-only properties
    public String getAccountNumber() { return accountNumber; }
    public String getHolderName()    { return holderName; }
    public double getBalance()       { return Math.round(balance * 100.0) / 100.0; }

    // Unmodifiable transaction history
    public List<String> getTransactionHistory() {
        return Collections.unmodifiableList(transactionLog);
    }

    // Controlled deposit (User requested snippet pattern!)
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            logTxn("DEPOSIT", amount);
        } else {
            System.out.println("  [WARN] Invalid deposit amount: " + amount);
        }
    }

    // Controlled withdrawal
    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) {
            System.out.println("  [WARN] Withdrawal denied: $" + amount);
            return false;
        }
        balance -= amount;
        logTxn("WITHDRAWAL", amount);
        return true;
    }

    // Static transfer (atomic โ€” both accounts updated or neither)
    public static boolean transfer(BankAccount from, BankAccount to, double amount) {
        if (from == null || to == null || amount <= 0 || from.balance < amount) return false;
        from.balance -= amount;
        to.balance   += amount;
        from.logTxn("TRANSFER OUT to " + to.accountNumber, amount);
        to.logTxn("TRANSFER IN from " + from.accountNumber, amount);
        return true;
    }

    private void logTxn(String type, double amount) {
        transactionLog.add(String.format("%-20s $%10.2f | Balance: $%10.2f", type, amount, balance));
    }

    @Override
    public String toString() {
        return String.format("BankAccount{#%s | %s | Balance: $%.2f}",
                accountNumber, holderName, balance);
    }
}

// =====================================================================
// PROJECT 2: STUDENT GRADE BOOK
// =====================================================================
class GradeBook {
    private final String studentName;
    private final Map<String, Integer> subjectGrades;

    public GradeBook(String studentName) {
        this.studentName   = studentName;
        this.subjectGrades = new LinkedHashMap<>();
    }

    public String getStudentName() { return studentName; }

    public void addGrade(String subject, int grade) {
        if (grade < 0 || grade > 100) throw new IllegalArgumentException("Grade must be 0โ€“100: " + grade);
        subjectGrades.put(subject.trim(), grade);
    }

    public Map<String, Integer> getGrades() {
        return Collections.unmodifiableMap(subjectGrades);
    }

    public double getAverage() {
        if (subjectGrades.isEmpty()) return 0.0;
        return subjectGrades.values().stream().mapToInt(Integer::intValue).average().orElse(0.0);
    }

    public char getLetterGrade() {
        double avg = getAverage();
        if (avg >= 90) return 'A';
        if (avg >= 80) return 'B';
        if (avg >= 70) return 'C';
        if (avg >= 60) return 'D';
        return 'F';
    }

    @Override
    public String toString() {
        return String.format("GradeBook{student='%s', avg=%.1f, grade='%c'}",
                studentName, getAverage(), getLetterGrade());
    }
}

// =====================================================================
// PROJECT 3: PRODUCT INVENTORY WITH ACCESS CONTROL
// =====================================================================
class InventoryItem {
    private static int idCounter = 0;
    private final String itemId;
    private String productName;
    private double unitPrice;
    private int stockQuantity;
    private boolean discontinued;

    public InventoryItem(String productName, double unitPrice, int initialStock) {
        this.itemId        = "ITEM-" + String.format("%03d", ++idCounter);
        setProductName(productName);
        setUnitPrice(unitPrice);
        this.stockQuantity = Math.max(0, initialStock);
        this.discontinued  = false;
    }

    public String getItemId()       { return itemId; }
    public String getProductName()  { return productName; }
    public double getUnitPrice()    { return unitPrice; }
    public int getStockQuantity()   { return stockQuantity; }
    public boolean isDiscontinued() { return discontinued; }
    public double getTotalValue()   { return unitPrice * stockQuantity; } // Computed

    public void setProductName(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required.");
        this.productName = name.trim();
    }

    public void setUnitPrice(double price) {
        if (price < 0) throw new IllegalArgumentException("Price cannot be negative.");
        this.unitPrice = price;
    }

    public boolean sell(int qty) {
        if (discontinued) { System.out.println("  [WARN] Item is discontinued!"); return false; }
        if (qty <= 0 || qty > stockQuantity) return false;
        stockQuantity -= qty;
        return true;
    }

    public void restock(int qty) {
        if (qty > 0) stockQuantity += qty;
    }

    public void discontinue() { this.discontinued = true; }

    @Override
    public String toString() {
        return String.format("[%s] %-22s | $%6.2f | Stock: %3d | Discontinued: %b",
                itemId, productName, unitPrice, stockQuantity, discontinued);
    }
}

// =====================================================================
// MAIN ORCHESTRATOR
// =====================================================================
public class Main {
    public static void main(String[] args) {
        // ---- PROJECT 1 ----
        System.out.println("โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—");
        System.out.println("โ•‘    PROJECT 1: SECURE BANK ACCOUNT    โ•‘");
        System.out.println("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
        BankAccount alice = new BankAccount("Alice Sharma", 5000.0);
        BankAccount bob   = new BankAccount("Bob Reddy",    3000.0);

        alice.deposit(2000.0);
        alice.deposit(-50.0);   // Invalid
        alice.withdraw(800.0);
        BankAccount.transfer(alice, bob, 1500.0);

        System.out.println("
" + alice);
        System.out.println(bob);
        System.out.println("
Alice's Transaction History:");
        alice.getTransactionHistory().forEach(t -> System.out.println("  " + t));

        // ---- PROJECT 2 ----
        System.out.println("
โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—");
        System.out.println("โ•‘    PROJECT 2: STUDENT GRADE BOOK     โ•‘");
        System.out.println("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
        GradeBook gb = new GradeBook("Ravi Kumar");
        gb.addGrade("Mathematics",   88);
        gb.addGrade("Physics",       92);
        gb.addGrade("Chemistry",     79);
        gb.addGrade("Computer Sci",  95);
        gb.addGrade("English",       83);

        System.out.println(gb);
        System.out.println("Subject Grades: " + gb.getGrades());
        try { gb.addGrade("History", 150); } catch (IllegalArgumentException e) {
            System.out.println("Grade error: " + e.getMessage());
        }

        // ---- PROJECT 3 ----
        System.out.println("
โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—");
        System.out.println("โ•‘    PROJECT 3: PRODUCT INVENTORY      โ•‘");
        System.out.println("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
        InventoryItem kb  = new InventoryItem("Mechanical Keyboard", 79.99, 100);
        InventoryItem mse = new InventoryItem("Wireless Mouse",       29.99, 250);
        InventoryItem mon = new InventoryItem("4K Monitor",          349.00,  30);

        System.out.println(kb);
        kb.sell(25);
        kb.restock(50);
        mon.discontinue();
        mon.sell(5); // Should warn!

        System.out.println("
Updated Inventory:");
        System.out.println(kb);
        System.out.println(mse);
        System.out.println(mon);
        System.out.printf("Total Inventory Value: $%.2f%n",
                kb.getTotalValue() + mse.getTotalValue() + mon.getTotalValue());
    }
}
๐Ÿ’ป Program Console Output
โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ PROJECT 1: SECURE BANK ACCOUNT โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• [WARN] Invalid deposit amount: -50.0 BankAccount{#ACC-1001 | Alice Sharma | Balance: $4700.00} BankAccount{#ACC-1002 | Bob Reddy | Balance: $4500.00} Alice's Transaction History: ACCOUNT OPENED $ 5000.00 | Balance: $ 5000.00 DEPOSIT $ 2000.00 | Balance: $ 7000.00 WITHDRAWAL $ 800.00 | Balance: $ 6200.00 TRANSFER OUT to ACC-1002 $ 1500.00 | Balance: $ 4700.00 โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ PROJECT 2: STUDENT GRADE BOOK โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• GradeBook{student='Ravi Kumar', avg=87.4, grade='B'} Subject Grades: {Mathematics=88, Physics=92, Chemistry=79, Computer Sci=95, English=83} Grade error: Grade must be 0โ€“100: 150 โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ PROJECT 3: PRODUCT INVENTORY โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• [ITEM-001] Mechanical Keyboard | $79.99 | Stock: 100 | Discontinued: false [WARN] Item is discontinued! Updated Inventory: [ITEM-001] Mechanical Keyboard | $79.99 | Stock: 125 | Discontinued: false [ITEM-002] Wireless Mouse | $29.99 | Stock: 250 | Discontinued: false [ITEM-003] 4K Monitor | $349.00 | Stock: 30 | Discontinued: true Total Inventory Value: $27972.25

๐Ÿ” Line-by-Line Code Explanation

Collections.unmodifiableList(transactionLog)

Returns a read-only view of the log โ€” callers can iterate and read but cannot add or remove entries.

from.balance -= amount; to.balance += amount;

Atomic transfer inside a static method accessing both accounts' private balances via trusted internal access.

public double getTotalValue()

Computed property: derives total value from existing fields without a redundant backing field.

public boolean isDiscontinued()

Boolean getter uses "is" prefix per Java Beans convention โ€” required by Spring and Hibernate frameworks.

Practical Real-World Example

โ˜• PracticalApplication.java โ€” Industry Implementation
// Config Manager Project (Package-internal validator + Public API)
class AppConfig {
    private static final Map<String, String> settings = new LinkedHashMap<>();
    private static final Set<String> validKeys = Set.of(
        "APP_NAME", "MAX_USERS", "TIMEOUT_MS", "DEBUG_MODE");

    // Package-private validator (internal use only)
    static void validateKey(String key) {
        if (!validKeys.contains(key))
            throw new IllegalArgumentException("Unknown config key: " + key);
    }

    // Public API
    public static void set(String key, String value) {
        validateKey(key);
        settings.put(key, value);
    }

    public static String get(String key) {
        validateKey(key);
        return settings.getOrDefault(key, "NOT_SET");
    }

    public static Map<String, String> getAllSettings() {
        return Collections.unmodifiableMap(settings);
    }
}

public class PracticalApplication {
    public static void main(String[] args) {
        AppConfig.set("APP_NAME", "OurCompiler");
        AppConfig.set("MAX_USERS", "5000");
        AppConfig.set("TIMEOUT_MS", "3000");
        System.out.println("=== Application Configuration ===");
        AppConfig.getAllSettings().forEach((k, v) ->
            System.out.printf("  %-15s = %s%n", k, v));
    }
}
๐Ÿ’ป Practical Console Output
=== Application Configuration === APP_NAME = OurCompiler MAX_USERS = 5000 TIMEOUT_MS = 3000
โš ๏ธ Common Mistakes & Professional Best Practices
  • Adding a setAccountNumber() setter on an account โ€” account numbers must be final and immutable.
  • Letting callers do account.setBalance(account.getBalance() - amount) instead of account.withdraw(amount) โ€” leaks business logic.
  • Exposing the raw transaction ArrayList from getTransactionHistory() without wrapping it in Collections.unmodifiableList().
  • Not validating inputs at the boundary of public methods โ€” every public method is a trust boundary.
๐ŸŽฏ Hands-on Coding Challenge

Test your understanding by writing the code directly in your editor or running in our online Java compiler:

โ˜• Challenge.java
// Final Coding Challenge:
// Create a fully encapsulated Library class:
// 1. private Map<String, Boolean> books (ISBN -> isAvailable).
// 2. addBook(isbn): adds with isAvailable=true.
// 3. checkOut(isbn): marks false, returns false if not available.
// 4. returnBook(isbn): marks true.
// 5. getAvailableCount(): computed from map values.
// 6. getBooks(): unmodifiable view.

import java.util.*;

class Library {
    private final Map<String, Boolean> books = new LinkedHashMap<>();

    public void addBook(String isbn) { books.put(isbn, true); }

    public boolean checkOut(String isbn) {
        if (!books.getOrDefault(isbn, false)) return false;
        books.put(isbn, false);
        return true;
    }

    public void returnBook(String isbn) {
        if (books.containsKey(isbn)) books.put(isbn, true);
    }

    public long getAvailableCount() {
        return books.values().stream().filter(Boolean::booleanValue).count();
    }

    public Map<String, Boolean> getBooks() {
        return Collections.unmodifiableMap(books);
    }
}

public class Challenge {
    public static void main(String[] args) {
        Library lib = new Library();
        lib.addBook("978-001"); lib.addBook("978-002"); lib.addBook("978-003");
        lib.checkOut("978-002");
        System.out.println("Books: " + lib.getBooks());
        System.out.println("Available: " + lib.getAvailableCount());
    }
}

๐Ÿ’ก Frequently Asked Questions & Interview Insights

โ“ Is it OK to call setter methods from within the constructor?

Yes โ€” in fact it is encouraged! Calling setters from constructors reuses the validation logic and avoids code duplication. However, be careful with final fields: they must be set exactly once, so setter-based assignment to final fields must be done carefully.

โ“ Can private methods be called by subclasses?

No. Private methods are strictly limited to the declaring class. They are not inherited and cannot be called or overridden by subclasses. Use `protected` if subclasses need access.

โ“ What is the difference between encapsulation and abstraction?

Encapsulation hides the internal state and provides controlled access via methods (HOW data is protected). Abstraction hides complexity by providing a simple interface that shows WHAT an object can do without exposing HOW it does it.

๐Ÿš€ Quick Chapter Recap

  • All 4 projects demonstrate complete encapsulation: private fields, validated setters, computed properties, and unmodifiable collection exposure.
  • Identity fields (IDs, timestamps) must be private final with getters but NO setters.
  • Return Collections.unmodifiableList/Map() to expose collection state safely.
  • Business logic (withdraw, enroll, sell) belongs inside the class โ€” not in caller code.
  • Access modifiers are the enforcement mechanism of good object-oriented design boundaries.
โ† Prev: 46. Package Access & Class Design Back to Java Course Home โ†’
OC
Curated by Our Compiler Java Technical Editorial Team
Published for 2026 Academic & Enterprise Reference ยท 100% Free & Open Access