Records (since Java 16, finalized in Java 17) are a concise way to declare immutable data classes. The compiler automatically generates constructors, accessors, equals(), hashCode(), and toString() — eliminating boilerplate for data-centric types.

Basic Record

  public record Point(int x, int y) { }

Point p = new Point(3, 4);
System.out.println(p.x());       // 3  (accessor, not getX())
System.out.println(p.y());       // 4
System.out.println(p);           // Point[x=3, y=4]
System.out.println(p.equals(new Point(3, 4))); // true
  

A record implicitly:

  • Is final and cannot be extended
  • Has private final fields for each component
  • Provides a public canonical constructor matching the components
  • Provides accessor methods named after components (no get prefix)
  • Generates equals(), hashCode(), and toString() based on all components

Canonical Constructor

The compiler generates a constructor matching the component list. You can add validation with a compact constructor:

  public record Range(int start, int end) {
    public Range {
        if (start > end) {
            throw new IllegalArgumentException(
                "start (" + start + ") must be <= end (" + end + ")");
        }
    }
}
  

The compact constructor assigns fields automatically after it runs — no need to write this.start = start.

You can also define a non-compact constructor for alternative construction paths:

  public record Email(String address) {
    public Email(String address) {
        this.address = address.toLowerCase().trim();
    }
}
  

Custom Methods and Static Members

Records can include additional methods, static fields, and static factory methods:

  public record Rectangle(double width, double height) {
    public double area() {
        return width * height;
    }

    public double perimeter() {
        return 2 * (width + height);
    }

    public static Rectangle square(double side) {
        return new Rectangle(side, side);
    }

    public static final Rectangle UNIT = new Rectangle(1, 1);
}
  

Keep records focused on data — complex behavior belongs in service classes.

Implementing Interfaces

  public record Person(String name, int age) implements Comparable<Person> {
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

// Sorting
List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25)
);
people.stream().sorted().forEach(System.out::println);
  

Records work well as implementations of sealed interfaces:

  public sealed interface Shape permits Circle, Rectangle { }
public record Circle(double radius) implements Shape { }
public record Rectangle(double width, double height) implements Shape { }
  

Records vs Classes

Feature Record Class
Purpose Immutable data carrier General-purpose object
Mutability Immutable by design Mutable by default
Inheritance Cannot extend classes (extends Record) Can extend one class
Boilerplate Minimal Manual equals/hashCode/toString
Fields All final components Any modifier
Abstract Cannot be abstract Can be abstract

Local Records (Java 16+)

Records can be declared inside methods for scoped data grouping:

  public void processOrders(List<Order> orders) {
    record OrderSummary(String id, double total) { }

    var summaries = orders.stream()
        .map(o -> new OrderSummary(o.id(), o.calculateTotal()))
        .toList();
}
  

Limitations

  • Cannot extend other classes (implicitly extends java.lang.Record)
  • Cannot declare instance fields beyond the record components
  • Are implicitly final — cannot be extended
  • Cannot be abstract
  • Cannot have instance initializers (use compact constructor instead)
  • Sensitive components appear in toString() — override if needed

When to Use Records

Use Case Example
DTOs record UserDto(Long id, String email, String role)
Value objects record Money(BigDecimal amount, Currency currency)
Multi-return values record SearchResult(List<Item> items, int totalCount)
Map keys Records with proper equals/hashCode
API responses record ApiResponse(int status, String body)
  public record UserDto(Long id, String email, String role) { }

// Safe as map key
Map<UserDto, List<Order>> ordersByUser = new HashMap<>();
ordersByUser.put(new UserDto(1L, "[email protected]", "admin"), List.of());
  

Serialization and JSON

Records work with Jackson, Gson, and JSON-B when configured properly:

  // Jackson deserializes via canonical constructor
public record Product(String name, BigDecimal price) { }

// JSON: {"name":"Widget","price":9.99}
ObjectMapper mapper = new ObjectMapper();
Product p = mapper.readValue(json, Product.class);
  

Ensure JSON property names match record component names (or use annotations).

Best Practices

  • Keep records focused on data — put complex behavior in separate service classes
  • Use compact constructors for validation and normalization
  • Prefer records over Lombok @Value for simple immutable data types
  • Override toString() if components contain sensitive data (passwords, tokens)
  • Use records with sealed interfaces for algebraic data types
  • Avoid making every class a record — mutable entities with identity (JPA entities) still need classes

Troubleshooting

Issue Cause Fix
“invalid accessor method” Trying to name accessor getX() Use component name: x() not getX()
Jackson deserialization fails Missing canonical constructor match Align JSON fields with component names
Cannot add mutable field Records only allow components Use a regular class instead
equals surprises with arrays Array components compared by reference Use List instead of array components
Record not immutable Mutable component type (e.g., List) Wrap with List.copyOf() in constructor

Records reduce boilerplate for the data-heavy patterns that dominate modern Java applications — DTOs, value objects, and sealed type variants — while maintaining full immutability and correct equality semantics.