Definition
A Java record (Java 16) is an immutable data carrier class.
- Auto-generates: canonical constructor, accessor methods (
name()notgetName()),equals()based on all components,hashCode()based on all components,toString()showing all components. - Immutable: all fields are implicitly
private final. No setters. - Final: records cannot be extended. Enforces value semantics.
- Implements interfaces: records can implement interfaces but cannot extend classes (already extend
Record). - Compact constructor: validation logic without repeating field assignments.
- Not suitable when: you need mutable state (setters), inheritance hierarchy (records are final), or JPA entities (require no-arg constructor + mutable fields).
A record is like a sealed bank receipt at National Bank of India: once printed (constructed), it cannot be modified. Two receipts with the same transaction details are considered identical (equals). The receipt automatically shows all details (toString). You cannot create a special receipt subclass (final). Records are perfect for DTOs: AccountSummary(accountId, balance, status) is data that flows between layers and is never modified.
Understanding the Concepts
What are Java Records?
Records replace POJO boilerplate with a single line.
What does a record generate?
// One line:
public record AccountSummary(String accountId, java.math.BigDecimal balance, String status) {}
// Compiler generates (equivalent to ~50 lines of POJO):
// - private final fields: accountId, balance, status
// - Canonical constructor
// - Accessors: accountId(), balance(), status() (not getAccountId)
// - equals() based on all components
// - hashCode() based on all components
// - toString(): AccountSummary[accountId=SAV2001, balance=125000, status=ACTIVE]
How do you add validation?
public record AccountSummary(String accountId, java.math.BigDecimal balance, String status) {
// Compact constructor: validation without repeating assignments
public AccountSummary {
if (accountId == null || accountId.isBlank()) throw new IllegalArgumentException("accountId required");
if (balance.signum() < 0) throw new IllegalArgumentException("balance cannot be negative");
// No this.accountId = accountId needed: compiler adds it.
}
// Custom methods
public boolean isHighValue() {
return balance.compareTo(new java.math.BigDecimal("100000")) > 0;
}
// Static factory
public static AccountSummary of(String id, double amount) {
return new AccountSummary(id, java.math.BigDecimal.valueOf(amount), "ACTIVE");
}
}
How do records enforce OOP principles?
// Encapsulation: fields are private final, no setters
AccountSummary summary = new AccountSummary("SAV2001", new java.math.BigDecimal("125000"), "ACTIVE");
// summary.accountId = "HACKED"; // compile error: private final
// No setAccountId() exists.
// Value semantics: same data = equal
AccountSummary s1 = new AccountSummary("SAV2001", new java.math.BigDecimal("125000"), "ACTIVE");
AccountSummary s2 = new AccountSummary("SAV2001", new java.math.BigDecimal("125000"), "ACTIVE");
s1.equals(s2); // true: same component values
s1.hashCode() == s2.hashCode(); // true: works in HashSet/HashMap
// Immutability: safe across threads, no synchronization needed
When are records NOT suitable?
// Not for JPA entities:
// @Entity public record Account(String id, BigDecimal balance) {}
// 1. JPA requires no-arg constructor: records do not have one.
// 2. JPA needs setters for dirty checking: records are immutable.
// 3. JPA proxies need non-final class: records are final.
// Not for mutable domain objects:
// You need account.setBalance(newBalance): records do not support this.
// Not for inheritance hierarchies:
// Records are final. class SpecialTrade extends Trade { } // compile error.
// Perfect for:
// DTOs: record TradeResponse(String tradeId, BigDecimal amount, String status) {}
// Value objects: record Money(BigDecimal amount, String currency) {}
// Map keys: record CompositeKey(String accountId, java.time.LocalDate date) {}
// Event payloads: record TradeExecutedEvent(String tradeId, BigDecimal amount) {}
How do records compare to regular classes?
| Aspect | Record | Regular Class |
|--------|--------|---------------|
| Mutability | Immutable (private final) | Configurable |
| Boilerplate | One line | 50+ lines |
| Inheritance | Cannot extend (final) | Can extend |
| Interfaces | Can implement | Can implement |
| equals/hashCode | Auto (all components) | Manual |
| JPA compatible | No | Yes |
| Use case | DTOs, value objects | Entities, services |
What should a senior engineer know?
- In Spring Boot, records work for @ConfigurationProperties and request/response DTOs.
- Records with sealed interfaces create algebraic data types: sealed interface TransactionResult permits Success, Failure { }.
- The compact constructor eliminates a common bug: forgetting to include a field in equals() or hashCode().
Visual Overview
RECORD vs POJO
──────────────────────────────────────────
Record (1 line):
public record AccountSummary(
String accountId,
BigDecimal balance,
String status
) {}
Generates:
┌─────────────────────────────────────┐
│ private final String accountId │
│ private final BigDecimal balance │
│ private final String status │
│ │
│ AccountSummary(id, balance, status) │ constructor
│ accountId() balance() status() │ accessors
│ equals(Object) hashCode() │ based on ALL
│ toString() │ shows ALL
└─────────────────────────────────────┘
POJO: 50+ lines for the same thing.
Key Takeaway
Java records enforce OOP principles automatically: encapsulation through private final fields with no setters (true immutability), and value semantics through auto-generated equals and hashCode based on all components. One line record AccountSummary(String accountId, BigDecimal balance, String status) {} replaces 50+ lines of POJO boilerplate. Records are implicitly final (cannot be extended) and their components are implicitly private final.
Records are not suitable for JPA entities (need no-arg constructor, mutable fields, non-final class), mutable domain objects (no setters), or inheritance hierarchies (records are final). Records are perfect for DTOs, value objects, composite Map keys, and event payloads.
Remember one sentence: records are immutable data carriers that auto-generate equals, hashCode, and toString from one line of code, and they enforce encapsulation and value semantics by design.
---