The full picture
public record Money(BigDecimal amount, Currency currency) {
public Money { // compact constructor
Objects.requireNonNull(currency);
if (amount.scale() > currency.getDefaultFractionDigits())
throw new IllegalArgumentException("scale");
amount = amount.setScale(currency.getDefaultFractionDigits()); // normalize
}
public Money add(Money other) { // behavior is welcome
if (!currency.equals(other.currency)) throw new IllegalArgumentException();
return new Money(amount.add(other.amount), currency);
}
}
// Shallow immutability trap:
record Batch(List<Item> items) {
Batch { items = List.copyOf(items); } // copy or stay mutable!
}- Where they shine: DTOs, API request/response bodies, compound map keys, domain value objects, multi-value returns, messages between threads.
- Why not JPA entities: entities have identity + mutable lifecycle + lazy proxies — the exact opposite of a record's contract (no no-arg ctor, final fields).
- vs Lombok @Data/@Value: records are a language feature with semantics (serialization uses the canonical constructor — validation runs on deserialize!, pattern-matching deconstruction); Lombok is codegen with none of those guarantees.
- Local records inside methods are underrated for naming intermediate stream results.