Technical Briefing
Java Records are a compelling choice for data-carrying types: concise, immutable, and expressive. But they are fundamentally incompatible with JPA's @Entity requirement. Hibernate needs to subclass your entity to create lazy-loading proxies, and it needs a no-args constructor to instantiate entities via reflection. Records are final classes with no no-args constructor โ both requirements are violated. The good news: Records excel as DTOs and projections in Hibernate 6, where they are directly supported.
โ Signal Detected
// Attempting to annotate a Record as a JPA Entity class="hi-ann">@Entity public record User(class="hi-ann">@Id Long id, String name, String email) {} // Runtime exception on startup: org.hibernate.MappingException: Could not instantiate tuplizer [org.hibernate.tuple.entity.PojoEntityTuplizer] for entity [com.example.entity.User] Caused by: java.lang.NoSuchMethodException: com.example.entity.User.<init>() // Hibernate requires a no-args constructor โ Records don't have one
โ Trace Analysis
JPA mandates that entity classes be non-final (Hibernate must subclass them to generate proxies for lazy loading), have a no-args constructor (Hibernate uses reflection to instantiate them), and support mutable state (Hibernate's dirty-checking mechanism modifies field values). All three requirements are directly violated by Java Records.
โฆ Remediation Plan
Keep @Entity classes as standard POJOs (or use Lombok @Data/@Builder to reduce boilerplate).
Use Records as DTOs in your API layer โ they are perfect for immutable request/response bodies.
Use Records as constructor expression targets in JPQL: SELECT new com.example.dto.UserDto(u.id, u.name) FROM User u.
Use Records as Spring Data interface projection types โ Hibernate 6 maps them automatically.
Use @Embeddable Records for value objects (Address, Money) in Hibernate 6.2+ โ this is officially supported.
// โ ILLEGAL โ Record cannot be a JPA Entity class="hi-ann">@Entity public record UserEntity(class="hi-ann">@Id Long id, String email) {} // โ CORRECT โ Standard POJO Entity (with Lombok) class="hi-ann">@Entity class="hi-ann">@Table(name = "users") class="hi-ann">@Getter class="hi-ann">@Setter class="hi-ann">@NoArgsConstructor public class User { class="hi-ann">@Id class="hi-ann">@GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; } // โ RECORD AS DTO (API response โ perfect use case) public record UserDto(Long id, String name) {} // โ RECORD IN JPQL CONSTRUCTOR EXPRESSION (Hibernate 6+) class="hi-ann">@Query(""" SELECT new com.example.dto.UserDto(u.id, u.name) FROM User u WHERE u.active = true """) List<UserDto> findActiveUsers(); // โ RECORD AS @Embeddable (Hibernate 6.2+ supported!) class="hi-ann">@Embeddable public record Address(String street, String city, String postcode) {} class="hi-ann">@Entity public class User { class="hi-ann">@Id private Long id; class="hi-ann">@Embedded private Address address; // Record embedded directly! }
โ Engineering Deep-Dive
Why Hibernate Needs Subclassing
When you call entityManager.getReference(User.class, id) or access a lazy class="hi-ann">@ManyToOne, Hibernate returns a proxy โ a CGLIB-generated subclass of your entity. This proxy contains only the ID; it fetches the full data on first property access. Since record is a final class, CGLIB cannot extend it, making lazy loading impossible.
Hibernate 6 Record Support
Hibernate 6.2 (shipped with Spring Boot 3.1+) added official support for Records as class="hi-ann">@Embeddable types. This is ideal for Domain-Driven Design value objects. The Record must be class="hi-ann">@Embeddable, not class="hi-ann">@Entity, and Hibernate instantiates it via its canonical constructor rather than reflection.
Spring Data Projections with Records
Spring Data JPA supports class-based projections (closed projections) via Records when you use constructor expressions. Alternatively, interface projections are proxy-based and work dynamically โ but Records as constructor targets give you type-safe, IDE-refactorable projections with zero runtime overhead.
โ Elite Standards
Engineering Rule
Use MapStruct to generate compile-time safe Entity-to-Record mappers โ avoid manual field-by-field copying in service methods.
Engineering Rule
Define all API request/response types as Records โ they enforce immutability and eliminate defensive copying.
Engineering Rule
Use Records as constructor projection targets in JPQL instead of full entity loads for any read-only endpoint.
FAQ
- What causes Java Records & JPA: The Compatibility Guide in Spring Boot 3?
- JPA mandates that entity classes be non-final (Hibernate must subclass them to generate proxies for lazy loading), have a no-args constructor (Hibernate uses reflection to instantiate them), and support mutable state (Hibernate's dirty-checking mechanism modifies field values). All three requirements are directly violated by Java Records.
- How do I fix Java Records & JPA: The Compatibility Guide?
- Keep @Entity classes as standard POJOs (or use Lombok @Data/@Builder to reduce boilerplate). Use Records as DTOs in your API layer โ they are perfect for immutable request/response bodies. Use Records as constructor expression targets in JPQL: SELECT new com.example.dto.UserDto(u.id, u.name) FROM User u. Use Records as Spring Data interface projection types โ Hibernate 6 maps them automatically. Use @Embeddable Records for value objects (Address, Money) in Hibernate 6.2+ โ this is officially supported.
- Best practice #1 for preventing Java 21 ยท JPA errors?
- Use MapStruct to generate compile-time safe Entity-to-Record mappers โ avoid manual field-by-field copying in service methods.
- Best practice #2 for preventing Java 21 ยท JPA errors?
- Define all API request/response types as Records โ they enforce immutability and eliminate defensive copying.
- Best practice #3 for preventing Java 21 ยท JPA errors?
- Use Records as constructor projection targets in JPQL instead of full entity loads for any read-only endpoint.