Back to Records
Q1Question 1 of 1

What are Java Records?

Key Concepts
  • record Trade(String id, BigDecimal amount) {} replaces 50+ lines of POJO with auto-generated constructor, accessors, equals, hashCode, toString.
  • Accessor methods use component names (id(), amount()), not JavaBean convention (getId(), getAmount()).
  • All fields are private final. Records are immutable, cannot have mutable instance fields, and are implicitly final.
  • Compact constructor Trade { validate(); } validates without repeating parameter assignments.
  • Records make excellent Map keys because equals and hashCode are correctly generated from all components.
  • Records cannot extend classes but can implement interfaces, add static fields, and add instance methods.
  • Use records for DTOs, value objects, and events. Use regular classes for mutable entities and inheritance.

Definition

`record Trade(String id, BigDecimal amount) {}` = immutable, auto equals/hashCode/toString, accessor methods id()/amount(). Use for DTOs and value objects. Java 16+.

A record is a compact, immutable data carrier class introduced in Java 16. record Trade(String id, BigDecimal amount) {} auto-generates: (1) private final fields for each component, (2) canonical constructor, (3) accessor methods (id(), amount() – NOT getId/getAmount), (4) equals() based on all components, (5) hashCode() based on all components, (6) toString() showing all components. Records are final (cannot be extended), cannot have instance fields beyond the components, and components are immutable (final). Use records for DTOs, value objects, tuples, and any class whose identity is defined by its data.

A record is like a bank receipt: it has fixed fields (date, amount, account), is SEALED (you can't add extra notes after printing), and two receipts with the same data are considered EQUAL. You don't write getDate()/equals()/hashCode()/toString() for every receipt – the receipt PRINTER (compiler) does it automatically.

Understanding the Concepts

What are Java Records?

A record is an immutable data carrier declared in one line. The compiler generates a canonical constructor, private final fields, public accessor methods (named after the components, not getX()), and value-based equals, hashCode, and toString. Records are implicitly final and cannot extend a class, but they can implement interfaces.

JAVA
package com.bank.model;

import java.math.BigDecimal;

// One line replaces 50+ lines of POJO boilerplate
public record TransferRequest(String fromAccount, String toAccount, BigDecimal amount) {}
  • The compiler generates:
  • private final String fromAccount, private final String toAccount, private final BigDecimal amount
  • TransferRequest(String fromAccount, String toAccount, BigDecimal amount) constructor
  • fromAccount(), toAccount(), amount() accessor methods (NOT getFromAccount)
  • equals() comparing all components
  • hashCode() based on all components
  • toString() showing all components

How do you validate with a compact constructor?

JAVA
package com.bank.model;

import java.math.BigDecimal;

public record TransferRequest(String fromAccount, String toAccount, BigDecimal amount) {
    // Compact constructor: no parameter list, no field assignments
    TransferRequest {
        if (fromAccount == null || fromAccount.isBlank()) {
            throw new IllegalArgumentException("From account is required");
        }
        if (amount.signum() <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        // Field assignments happen automatically AFTER this block
    }
}
JAVA
// Usage:
new TransferRequest("SAV2001", "SAV2002", new BigDecimal("50000")); // OK
new TransferRequest("", "SAV2002", new BigDecimal("50000"));        // throws IAE
new TransferRequest("SAV2001", "SAV2002", BigDecimal.ZERO);         // throws IAE

How do records compare to regular classes?

| Feature | Record | Regular Class |
|---------|--------|---------------|
| Boilerplate | 1 line | 50+ lines |
| Mutability | Immutable (final fields) | Mutable (setters) |
| equals/hashCode | Auto (all components) | Manual or IDE-generated |
| toString | Auto (all components) | Manual |
| Inheritance | Cannot extend classes | Can extend |
| Accessor naming | component() | getComponent() |
| Serializable | Yes (if components are) | Requires explicit |

How do you use records with streams?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class RecordStreamDemo {
    record Trade(String symbol, String side, BigDecimal amount) {}

    public static void main(String[] args) {
        List<Trade> trades = List.of(
            new Trade("RELIANCE", "BUY", new BigDecimal("45000")),
            new Trade("TCS", "SELL", new BigDecimal("78000")),
            new Trade("RELIANCE", "SELL", new BigDecimal("32000"))
        );

        // Records work perfectly as stream elements
        Map<String, BigDecimal> totalBySymbol = trades.stream()
            .collect(Collectors.groupingBy(Trade::symbol,
                Collectors.reducing(BigDecimal.ZERO, Trade::amount, BigDecimal::add)));

        System.out.println(totalBySymbol);
        // {RELIANCE=77000, TCS=78000}

        // Records make excellent Map keys because equals/hashCode are correct
        Map<Trade, String> tradeStatus = Map.of(
            new Trade("RELIANCE", "BUY", new BigDecimal("45000")), "SETTLED");

        // Two records with same components are equal
        Trade lookup = new Trade("RELIANCE", "BUY", new BigDecimal("45000"));
        System.out.println(tradeStatus.get(lookup)); // SETTLED
    }
}

What can records NOT do?

JAVA
// CANNOT extend a class (implicitly extends Record)
// public record Trade(...) extends BaseEntity {} // COMPILE ERROR

// CANNOT add mutable instance fields
// public record Trade(String id) { int counter = 0; } // COMPILE ERROR

// CAN implement interfaces
public record Trade(String id, BigDecimal amount) implements Serializable {}

// CAN add static fields and methods
public record Trade(String id, BigDecimal amount) {
    public static final Trade EMPTY = new Trade("", BigDecimal.ZERO);
    public static Trade of(String id, String amount) {
        return new Trade(id, new BigDecimal(amount));
    }
}

// CAN add instance methods
public record Trade(String id, BigDecimal amount) {
    public boolean isLarge() { return amount.compareTo(new BigDecimal("100000")) > 0; }
}

What are common mistakes?

JAVA
// WRONG: expecting getter naming convention
Trade t = new Trade("TRD-001", new BigDecimal("50000"));
// t.getId();     // COMPILE ERROR: no getId()
t.id();           // CORRECT: component name, not JavaBean style

// WRONG: trying to modify a record
// t.setAmount(new BigDecimal("60000")); // records have no setters
// CORRECT: create a new record
Trade updated = new Trade(t.id(), new BigDecimal("60000"));

// WRONG: assuming equals ignores BigDecimal scale
Trade t1 = new Trade("A", new BigDecimal("100"));
Trade t2 = new Trade("A", new BigDecimal("100.00"));
System.out.println(t1.equals(t2)); // false! BigDecimal.equals checks scale

What about record patterns (Java 21)?

JAVA
// Deconstruct records in instanceof and switch
Object obj = new Trade("TRD-001", new BigDecimal("50000"));

if (obj instanceof Trade(String id, BigDecimal amount)) {
    System.out.println("Trade " + id + ": Rs." + amount);
}

// Switch with record patterns
String describe(Object obj) {
    return switch (obj) {
        case Trade(String id, BigDecimal amt) when amt.compareTo(new BigDecimal("100000")) > 0
            -> "Large trade: " + id;
        case Trade(String id, BigDecimal amt)
            -> "Trade: " + id;
        default -> "Unknown";
    };
}

When should you use records vs classes?

| Use Case | Record | Class |
|----------|--------|-------|
| DTO / API response | Yes | No |
| Value object | Yes | No |
| Map key | Yes (correct equals) | Risky (manual equals) |
| Domain entity with behavior | No | Yes |
| Mutable state needed | No | Yes |
| Inheritance hierarchy | No | Yes |
| JPA entity | Caution (no no-arg constructor) | Yes |

What should a senior engineer know?

  • Records + sealed classes + pattern matching = modern Java's algebraic data types.
  • Use records for DTOs, value objects, events, and any class whose identity is its data.
  • BigDecimal.equals checks scale, so new BigDecimal("100").equals(new BigDecimal("100.00")) is false. Use compareTo for value equality in records containing BigDecimal.
  • Records are thread-safe because all fields are final. Safe to share across threads without synchronization.
  • JPA entities require a no-arg constructor, so records do not work directly as JPA entities. Use records for DTOs and projections instead.

Visual Overview

DIAGRAM
  Record auto-generation:
  ───────────────────────
  record Trade(String id, BigDecimal amount) {}
       │
       ▼ Compiler generates:
  ┌─────────────────────────────────────┐
  │ private final String id;            │
  │ private final BigDecimal amount;    │
  │                                     │
  │ Trade(String id, BigDecimal amount) │  canonical constructor
  │ String id()                         │  accessor (not getId)
  │ BigDecimal amount()                 │  accessor
  │ boolean equals(Object)              │  all components
  │ int hashCode()                      │  all components
  │ String toString()                   │  all components
  └─────────────────────────────────────┘
  Immutable. Final. Thread-safe.

Key Takeaway

Records (Java 16) are immutable data carriers that auto-generate constructor, accessor methods (id() not getId()), equals, hashCode, and toString from the component declaration. record Trade(String id, BigDecimal amount) {} replaces 50+ lines of POJO boilerplate.

Records are final, cannot extend classes, cannot have mutable instance fields, and component accessors use component names (not JavaBean naming). The compact constructor Trade { validate(); } enables validation without repeating parameter assignments.

Use records for DTOs, value objects, map keys, events, and any class whose identity is its data. Do not use them where you need mutability, inheritance, or encapsulated behavior-rich domain entities. Records + sealed classes + pattern matching form modern Java's algebraic data types.

Remember one sentence: record Trade(String id, BigDecimal amount) {} replaces 50+ lines of POJO with auto-generated immutable fields, equals, hashCode, and toString.

---

Share