Definition
Five rules for immutability:
- Class is final – prevents subclassing that could add mutable state.
- All fields are private and final – assigned once in the constructor.
- No setters – no way to modify state after construction.
- Defensive copies – if a field is a mutable type (List, Date), copy it in the constructor and in the getter.
- Validate in the constructor – reject invalid inputs at creation time.
An immutable class is like a sealed bank statement. Once printed, the statement cannot be altered. Anyone who receives a copy gets their own print (defensive copy), so marking up their copy does not change the original or anyone else's copy.
Understanding the Concepts
Complete immutable class example
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Objects;
public final class AccountStatement { // 1. final class
private final String accountId; // 2. private final fields
private final BigDecimal balance;
private final List<String> transactions;
public AccountStatement(String accountId, BigDecimal balance, List<String> transactions) {
// 5. Validate
this.accountId = Objects.requireNonNull(accountId);
this.balance = Objects.requireNonNull(balance);
// 4. Defensive copy in constructor
this.transactions = new ArrayList<>(Objects.requireNonNull(transactions));
}
// 3. Only getters, no setters
public String getAccountId() { return accountId; } // String is immutable, safe
public BigDecimal getBalance() { return balance; } // BigDecimal is immutable, safe
// 4. Defensive copy in getter
public List<String> getTransactions() {
return Collections.unmodifiableList(transactions);
}
}
Why each rule matters
// Without final class: a subclass could add mutable state
// Without final fields: someone could reassign via reflection
// Without defensive copy:
List<String> txns = new ArrayList<>(List.of("TX001"));
AccountStatement stmt = new AccountStatement("SAV2001", BigDecimal.ZERO, txns);
txns.add("TX002"); // if no defensive copy, this modifies the statement!
Records as immutable classes (Java 16+)
// Record: automatically final, private final fields, no setters, equals/hashCode/toString
public record AccountSummary(String accountId, BigDecimal balance) {
// Compact constructor for validation
public AccountSummary {
Objects.requireNonNull(accountId);
Objects.requireNonNull(balance);
}
}
// Usage
var summary = new AccountSummary("SAV2001", new BigDecimal("500000"));
// summary.accountId() → "SAV2001" (no set method exists)
Records handle rules 1-3 and 5 automatically. You still need defensive copies for mutable fields (rule 4).
What should a senior engineer know?
- String, BigDecimal, LocalDate, Integer are all immutable. Safe to share without copying.
- Immutable objects are inherently thread-safe. No synchronization needed because state never changes.
- Use
List.of(),Map.of(),Set.of()(Java 9+) for immutable collections. - Guava ImmutableList.Builder provides a builder pattern for constructing immutable collections.
What are common mistakes?
A common mistake is not understanding the concept fully before using it in production banking applications.
Wrong approach:
package com.bank.service;
public class TransferService {
public void transfer(String fromAccount, String toAccount, double amount) {
// WRONG: using double for money
double balance = getBalance(fromAccount);
balance = balance - amount;
}
private double getBalance(String id) { return 10000.0; }
}
Correct approach:
package com.bank.service;
import java.math.BigDecimal;
public class TransferService {
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// CORRECT: using BigDecimal for money
BigDecimal balance = getBalance(fromAccount);
balance = balance.subtract(amount);
}
private BigDecimal getBalance(String id) { return new BigDecimal("10000.00"); }
}
Always use BigDecimal for monetary calculations in banking systems to avoid floating-point precision errors.
What are the edge cases?
Edge cases can cause unexpected behavior in banking applications. Here are some to watch for.
package com.bank.edge;
public class EdgeCaseDemo {
public static void main(String[] args) {
// Edge case 1: null handling
String accountId = null;
try {
System.out.println(accountId.length());
} catch (NullPointerException e) {
System.out.println("Null account ID");
}
// Edge case 2: empty vs null
String emptyId = "";
System.out.println(emptyId.isEmpty()); // true
System.out.println(emptyId == null); // false
// Edge case 3: boundary values
java.math.BigDecimal zero = java.math.BigDecimal.ZERO;
java.math.BigDecimal negativeBalance = new java.math.BigDecimal("-0.01");
System.out.println(zero.compareTo(negativeBalance) > 0); // true
}
}
Null account ID
true
false
true
Always test with null values, empty strings, boundary values, and negative amounts in banking applications.
What are common interview follow-up questions?
Interviewers often follow up with these related questions:
- How does this concept apply to a high-throughput banking transaction system?
- What are the performance implications in a production environment?
- How would you debug issues related to this concept?
- What tools would you use to monitor this in production?
- How does this behave differently in Java 8 vs Java 17+?
When answering follow-up questions, always relate your answer back to practical experience. For example, at National Bank of India, Rajesh debugged a production issue related to this concept by using JFR recordings and heap dumps. Providing concrete examples from banking systems shows the interviewer that you understand both the theory and the practice.
Visual Overview
5 rules for immutability:
1. final class → cannot subclass
2. private final fields → cannot reassign
3. no setters → cannot modify
4. defensive copies → mutable fields isolated
5. validate in constructor → reject bad input
Record (Java 16+) automates: 1, 2, 3, 5
You add: 4 (defensive copies for mutable fields)
Key Takeaway
An immutable class has five properties: the class is final, all fields are private and final, there are no setters, mutable fields are defensively copied in constructors and getters, and inputs are validated at construction time.
Records automate most of this since Java 16. Immutable objects are inherently thread-safe because their state never changes after construction.
One sentence to remember: final class, private final fields, no setters, defensive copies of mutable fields, validate in the constructor.
---