Why I Can't Use Java Records as JPA Entities

Published: (December 13, 2025 at 05:40 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

Introduction

With the introduction of Java Records (officially in Java 16), developers finally got a concise way to create immutable data‑carrier classes without writing boilerplate code such as equals(), hashCode(), and toString().
Naturally, many of us looked at our verbose JPA entities and wondered:

“Can I replace these with records to make my code cleaner?”

The short answer is no.

Why Records Are Incompatible with JPA

Immutability vs. Mutability

  • Record philosophy – All fields are implicitly final; once an instance is created its state cannot change.
  • JPA requirement – Entities must be mutable. JPA manages the state of an entity by loading it from the database, allowing fields to be changed, and then persisting those changes.

Key JPA mechanisms that rely on mutability:

  • Dirty checking – Hibernate compares the current state of an object with its original snapshot.
  • Setters – JPA uses setters (or field access) to populate entity fields when reading from the database.

Since records lack setters and have final fields, JPA cannot manage their lifecycle or update their state.

Lazy Loading and Proxies

One of JPA’s most powerful features is lazy loading, which fetches related data only when it is accessed. To achieve this, providers such as Hibernate generate proxy classes that:

  1. Extend the original entity class.
  2. Override getter methods to trigger a database call on demand.

Records are final classes, which means they cannot be subclassed. Consequently, JPA cannot create proxies for records, and lazy loading (as well as other proxy‑based features) breaks completely.

Alternatives: Keep Entities Concise with Lombok

While records are unsuitable for JPA entities, you can still reduce boilerplate by using Lombok with regular classes.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.AccessLevel;

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // limit access to the default constructor
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private String email;
}

Note: The following record definition would cause issues with JPA because records are immutable and final.

// This will cause issues with JPA!
public record User(Long id, String name, String email) {}

Conclusion

  • Use standard classes (optionally with Lombok) for JPA entities to satisfy mutability, default constructors, and non‑final class requirements.
  • Use records for immutable data carriers such as DTOs, where JPA’s persistence features are not needed.
Back to Blog

Related posts

Read more »