Immutability & Value Objects
Definition

Benefits of Immutability

Creating Immutable Classes
// Recipe for immutable class
public final class Money { // 1. Class is final
private final BigDecimal amount; // 2. Fields are final
private final Currency currency;
// 3. All values set in constructor
public Money(BigDecimal amount, Currency currency) {
// 4. Validate
if (amount == null || currency == null) {
throw new IllegalArgumentException("Null values not allowed");
}
// 5. Defensive copy mutable inputs (not needed here - BigDecimal is immutable)
this.amount = amount;
this.currency = currency;
}
// 6. No setters
// 7. Getters return immutable values or copies
public BigDecimal getAmount() {
return amount; // BigDecimal is immutable, safe to return
}
public Currency getCurrency() {
return currency; // Currency is immutable
}
// 8. Operations return new instances
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), currency);
}
// 9. equals and hashCode based on values
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 &&
currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
Java Records (Java 16+)
// Records make immutable value objects easy
// Traditional way: ~50 lines
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
// With records: 1 line!
public record Point(int x, int y) { }
// Records are:
// - Immutable (final fields)
// - equals/hashCode based on all fields
// - toString auto-generated
// - Can add methods and validation
public record Money(BigDecimal amount, Currency currency) {
// Compact constructor for validation
public Money {
if (amount == null || currency == null) {
throw new IllegalArgumentException("Null not allowed");
}
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Negative not allowed");
}
}
// Additional methods
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount.add(other.amount), currency);
}
}
Value Objects in Practice
// Instead of primitive obsession, use value objects
// BAD: Primitive obsession
public class Order {
private String customerId; // What format? Validated?
private String email; // Valid email?
private double total; // What currency?
private String status; // What values allowed?
}
// GOOD: Rich value objects
public class Order {
private CustomerId customerId;
private EmailAddress email;
private Money total;
private OrderStatus status;
}
// Each value object validates itself
public record CustomerId(String value) {
public CustomerId {
if (value == null || !value.matches("^CUST-\\d{6}$")) {
throw new IllegalArgumentException("Invalid customer ID: " + value);
}
}
}
public record EmailAddress(String value) {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[\\w.-]+@[\\w.-]+\\.\\w+$");
public EmailAddress {
if (value == null || !EMAIL_PATTERN.matcher(value).matches()) {
throw new InvalidEmailException(value);
}
value = value.toLowerCase(); // Normalize
}
}
public enum OrderStatus {
PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}
// Now impossible to create order with invalid email!
With Methods Pattern
// Modifying immutable objects using "with" methods
public record User(
UserId id,
String name,
EmailAddress email,
boolean active
) {
// "With" methods return modified copy
public User withName(String newName) {
return new User(id, newName, email, active);
}
public User withEmail(EmailAddress newEmail) {
return new User(id, name, newEmail, active);
}
public User withActive(boolean newActive) {
return new User(id, name, email, newActive);
}
}
// Usage
User user = new User(id, "John", email, true);
User updated = user
.withName("John Smith")
.withEmail(new EmailAddress("[email protected]"));
// Original user unchanged!
// Lombok can generate these automatically:
@With
@Value // Makes class immutable with getters
public class User {
UserId id;
String name;
EmailAddress email;
boolean active;
}
Entity vs Value Object

Tips & Tricks
