Java Encapsulation Capstone: 4 Production-Grade Secure Systems
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 PrivilegeBeginner Example & Code Anatomy
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());
}
}
๐ 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
// 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));
}
}
- Adding a
setAccountNumber()setter on an account โ account numbers must be final and immutable. - Letting callers do
account.setBalance(account.getBalance() - amount)instead ofaccount.withdraw(amount)โ leaks business logic. - Exposing the raw transaction
ArrayListfromgetTransactionHistory()without wrapping it inCollections.unmodifiableList(). - Not validating inputs at the boundary of public methods โ every public method is a trust boundary.
Test your understanding by writing the code directly in your editor or running in our online Java compiler:
// 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 finalwith 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.