Java Records & JPA: A Match Made in Heaven... or a Trap?

With modern Java, we all want to use records. They are concise, immutable, and clean.

So, the obvious question arises: “Can I just annotate my record with @Entity and use it with JPA?”

The short answer: No, and you shouldn’t want to.

The Fundamental Mismatch

JPA (and Hibernate) and Java Records represent two entirely different paradigms.

JPA expects a mutable bean

JPA needs a no-args constructor to create an empty object via reflection. Then, it uses setters (or field reflection) to hydrate that object with data pulled from the database. Throughout its lifecycle, the entity’s state can change.

A record is fundamentally immutable

A record’s state is set exactly once, at creation time, via its canonical constructor. It has no setters. It has no no-args constructor by default.

They are designed for opposite purposes. Trying to force them together leads to workarounds that break the very purpose of using a record.

// This is bad practice! Do not force records into entities.
@Entity
public record UserEntity(
    @Id @GeneratedValue Long id,
    String name
) {
    // You have to write custom constructors and hacks to make Hibernate happy.
}

The Correct Architecture

Keep your persistence layer separate from your domain/data-transfer layer.

Use standard, mutable classes for your JPA Entities:

@Entity
public class UserEntity {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    
    // ... getters, setters, no-args constructor
}

Then, use Records where they shine: as immutable Data Transfer Objects (DTOs) or API responses:

public record UserDto(Long id, String name) {}

Map the UserEntity to the UserDto at your service boundary. This keeps Hibernate happy, keeps your data transfers immutable, and maintains a clean separation of concerns.