Java Records & JPA: The Compatibility Guide

Why Java Records cannot be used as @Entity classes — and how to leverage them as first-class DTOs and projections in Hibernate 6.

Framework
Spring Boot 3.x
Runtime
Java 21 LTS
Stability
Enterprise grade

Overview

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.

Symptom

exception_report.logFatal
// 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

Root cause

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.

Resolution

  1. Keep @Entity classes as standard POJOs (or use Lombok @Data/@Builder to reduce boilerplate).

  2. Use Records as DTOs in your API layer — they are perfect for immutable request/response bodies.

  3. Use Records as constructor expression targets in JPQL: SELECT new com.example.dto.UserDto(u.id, u.name) FROM User u.

  4. Use Records as Spring Data interface projection types — Hibernate 6 maps them automatically.

  5. Use @Embeddable Records for value objects (Address, Money) in Hibernate 6.2+ — this is officially supported.

Production implementation
SafeJava 21
// ❌ 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!
}

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.

Best practices

  1. Use MapStruct to generate compile-time safe Entity-to-Record mappers — avoid manual field-by-field copying in service methods.

  2. Define all API request/response types as Records — they enforce immutability and eliminate defensive copying.

  3. 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.

Advanced Persistence Patterns with Java Records in Spring Data

Java records and JPA entities occupy fundamentally different design spaces. A record is an immutable, value-oriented type: its fields are set once at construction, it has value-based equality, and it carries no hidden mutable state. A JPA entity is a lifecycle-managed, identity-oriented type: Hibernate must be able to instantiate it without arguments and track its dirty state between transactions. These two contracts are in direct tension — which is why @Entity on a record produces a suite of runtime failures rather than a clean error at compile time.

✦ Records as JPA Projections — the Sweet Spot

public record ProductSummary(Long id, String name, BigDecimal price) {}

@Query("""
  SELECT new com.example.ProductSummary(p.id, p.name, p.price)
  FROM Product p WHERE p.active = true
""")
List<ProductSummary> findActiveSummaries();

Hibernate constructs the result set directly into the record's constructor without creating a managed entity, tracking dirty state, or populating lazy associations. For read-heavy endpoints this pattern can reduce query time by 30–50% by eliminating persistence context overhead.

Records vs Interface Projections

Interface projections use JDK dynamic proxies — a small overhead per field access. Record projections are plain Java objects: no proxy, no reflection, fully serializable without Jackson config. For high-frequency read paths, records are the superior choice.

📬

Records as DTOs

Using records as Data Transfer Objects between your service and API layer eliminates an entire class of mutable-DTO bugs. The canonical constructor ensures every field is initialized, toString() is safe for logging, and equals()/hashCode() make AssertJ assertions trivially correct.

🌲

Inheritance Limitation

Records cannot extend classes, ruling them out for JPA inheritance strategies. If your domain requires polymorphic persistence, keep traditional entities for the persistence layer and map to sealed interfaces with record implementations at the service boundary.

Records do not replace entities — they complement them. The discipline of separating what the database stores (entities) from what the application computes and exposes (records and DTOs) produces a codebase where persistence concerns are isolated, business logic is testable without a database, and API shapes can evolve independently of schema changes.

Kodivio Team
Kodivio Team

AI & Web Development Specialists

The Kodivio team covers AI tools, automation, and modern web development based on real-world testing and hands-on experience.

Learn more about us →