Back to Modern Java OOP: Records & Sealed
Q1Question 1 of 2

What are Java Records?

Key Concepts
  • A record is an immutable data carrier: one line generates constructor, accessors, equals, hashCode, and toString
  • All record fields are implicitly private final with no setters: true encapsulation by design
  • Records are implicitly final: they cannot be extended, establishing value semantics
  • Records can implement interfaces but cannot extend classes (they already extend java.lang.Record)
  • Perfect for: DTOs, value objects, API responses, composite Map keys, event payloads
  • Not suitable for: JPA entities (need no-arg constructor and mutable fields), mutable domain objects, inheritance hierarchies

Definition

A record is an immutable, final, auto-generated data carrier: one line replaces 50 lines of POJO. Perfect for DTOs and value objects. Not suitable for JPA entities, mutable domain objects, or inheritance hierarchies.

A Java record (Java 16) is an immutable data carrier class.

  • Auto-generates: canonical constructor, accessor methods (name() not getName()), 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?

JAVA
// 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?

JAVA
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?

JAVA
// 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?

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate