What Are Java Records?
Java Records are a special kind of class introduced as a preview feature in Java 14 (JEP 359), refined in Java 15, and finalized in Java 16 (JEP 395). A record is a transparent, shallowly immutable carrier for a fixed set of values. Where a traditional POJO (Plain Old Java Object) might span dozens of lines of boilerplate — fields, constructor, getters, equals, hashCode, and toString — a record expresses the same data contract in a single declarative line.
public record User(Long id, String name, String email) {}
The Java compiler synthesizes a canonical constructor, accessor methods (id(), name(), email()), and value-based equals, hashCode, and toString implementations. Records are implicitly final, their components are private final, and they extend java.lang.Record rather than java.lang.Object. This makes records ideal for data carriers such as DTOs, value objects, tuples, map keys, and event payloads.
Java Records vs POJOs
A POJO is the historical Java pattern for representing data: a class with a no-arg constructor, mutable fields, and a getter and setter for each field. POJOs are flexible, work with virtually every Java framework that relies on reflection (JPA, JAXB, older Jackson versions, JavaBeans introspection, BeanUtils, Spring binding), but they carry significant ceremonial overhead. Records sacrifice some of that flexibility — they cannot be extended, their fields cannot be reassigned, and they cannot have a default no-arg constructor — in exchange for conciseness, immutability, and value semantics.
The practical differences show up in three places. First, mutability: a POJO's setter can be invoked after construction, which makes it suitable for frameworks that build instances incrementally (Hibernate hydration, Spring form binding, builder-style deserializers); a record is sealed once constructed. Second, identity: two records are equal if every component is equal, which makes them natural map keys and cache keys; two POJOs are equal only if their author wrote equals and hashCode correctly. Third, memory layout: records are typically the same size as the equivalent POJO, but their final fields enable JVM optimizations such as scalar replacement and aggressive field reordering.
When To Use Records
Reach for a record whenever you need an immutable carrier for a small group of related values. The canonical use cases are:
- API response DTOs — the response body of a REST endpoint rarely needs to mutate after construction; a record makes the contract explicit.
- Domain events — events are facts about the past; they should never be modified after publication.
- Value objects — money, coordinates, date ranges, identifiers — anything whose meaning is defined entirely by its components.
- Tuples for stream pipelines — collect intermediate results without inventing throwaway classes.
- Map keys and cache keys — value-based
equalsandhashCodecome for free. - Configuration snapshots — properties loaded from YAML or environment variables that should not drift at runtime.
When Not To Use Records
Records are not a universal replacement for POJOs. Avoid them when:
- You need a JPA entity. Hibernate must instantiate the entity with a no-arg constructor and populate its fields via setters or reflection on mutable fields. A record cannot satisfy this contract.
- You need inheritance. Records are implicitly final and cannot extend another class. If your domain model relies on a class hierarchy, use sealed classes or abstract base classes instead.
- You need partial construction. Builders that gradually accumulate state, form bindings that arrive one field at a time, or framework-driven hydration all assume mutability.
- You need to derive fields from other fields in a stateful way. Records support compact constructors for validation and normalization, but lazy or memoized properties usually feel awkward.
Records in Spring Boot
Spring Boot 3.x and Spring Framework 6 embrace Records throughout the request lifecycle. @RequestBody binding, @ConfigurationProperties, @RestController response bodies, and RestTemplate/WebClient deserialization all understand records natively through their Jackson 2.12+ integration. @ConfigurationProperties in particular is one of the most rewarding upgrades: a configuration snapshot loaded from application.yml becomes a single, immutable, validated record.
@ConfigurationProperties("app.mail")
public record MailProperties(String host, int port, String username, String password) {}
Spring Data JDBC also supports records as aggregate roots; Spring Data JPA does not, because the JPA specification mandates mutability. Spring MVC's URI templating, query parameter binding through @ModelAttribute, and validation via @Valid work with records as well.
Records and JPA
This is the most frequently asked compatibility question, and the answer is unambiguous: do not annotate a record with @Entity. The JPA 3.1 specification requires entity classes to declare a public or protected no-arg constructor, to be non-final, and to allow Hibernate to set field values via reflection on mutable fields. Records violate every one of these constraints by design.
What you should do instead is use records as DTO projections. Both JPQL and Spring Data support constructor-style projections that materialize records directly from a query.
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@Query("select new com.example.dto.UserSummary(u.id, u.name) from UserEntity u")
List<UserSummary> findAllSummaries();
}
This pattern gives you the best of both worlds: a mutable, framework-friendly entity at the persistence boundary, and a clean, immutable record at the API boundary.
Records and Jackson
Jackson 2.12 added first-class support for records. Serialization works out of the box because Jackson reads record components reflectively. Deserialization uses the canonical constructor, so there is no need for @JsonCreator or @JsonProperty annotations on every component in the common case. You can still annotate individual components to customize names, formats, or visibility:
public record Article(
@JsonProperty("article_id") Long id,
String title,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate publishedAt
) {}
For polymorphic deserialization, combine records with sealed interfaces and @JsonTypeInfo for a fully type-safe, exhaustive JSON model.
Records and Validation
Bean Validation (Jakarta Validation 3.0+) supports records natively. Place constraint annotations directly on record components, exactly as you would on POJO fields:
public record SignupRequest(
@NotBlank @Size(max = 80) String name,
@NotBlank @Email String email,
@NotBlank @Size(min = 8, max = 72) String password
) {}
The compact constructor is a great place to enforce cross-field invariants that cannot be expressed with a single annotation, such as "endDate must be after startDate". This validation runs as part of construction and cannot be bypassed.
Records and MapStruct
MapStruct 1.5+ supports records as both source and target types. Define a mapper interface and MapStruct generates the implementation at compile time, with zero reflection cost:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(UserEntity entity);
UserEntity toEntity(UserDto dto);
}
For records, MapStruct uses the canonical constructor; for entities, it uses setters. This is the recommended pattern for the boundary between persistence and API layers.
Java Record Best Practices
A few rules will keep your record-based code clean as your codebase grows:
- Validate in the compact constructor. It runs before fields are assigned and centralizes invariant checks.
- Normalize inputs in the compact constructor too — trim strings, copy mutable collections to
List.copyOf, lowercase emails. - Do not expose mutable collections as components. If a record carries a
List<String>, wrap it withList.copyOfso the immutability promise is real. - Prefer records for DTOs and value objects, POJOs for entities and mutable form models.
- Avoid records that carry more than seven or eight components; reach for nested records or value object decomposition first.
- Use static factory methods for alternative construction shapes — they read better than overloaded constructors and never conflict with the canonical constructor.
- Combine records with sealed interfaces for closed type hierarchies that the compiler can exhaustively check.
Frequently Asked Questions
What is a Java Record?
A Java Record is a transparent, shallowly immutable carrier class introduced as a preview in Java 14 and finalized in Java 16. The compiler generates the canonical constructor, accessors, equals, hashCode, and toString automatically.
What Java version do I need to use Records?
Records are a standard, non-preview feature from Java 16 onwards. They are fully supported in the Java 17, 21, and 25 LTS releases. Java 14 and 15 expose them as a preview feature behind --enable-preview.
Can a Record have methods?
Yes. Records can declare instance methods, static methods, static factory methods, and even override the canonical constructor with a compact constructor for validation or normalization.
Can a Record extend another class?
No. Records implicitly extend java.lang.Record and cannot extend any other class. They can, however, implement any number of interfaces.
Can a Record be a JPA entity?
No. JPA requires a no-arg constructor and mutable fields to manage entity state, both of which records intentionally lack. Use records for DTO projections instead.
Do Records work with Lombok?
Lombok's annotations like @Data, @Getter, and @Setter are largely redundant for records, which generate accessors and value semantics automatically. Lombok and records solve overlapping problems, so most teams use one or the other.
Can I add a Builder to a Record?
Yes, either by writing a static nested Builder class by hand or by relying on tools that generate one. Builders are most useful when records have many components or many optional fields.
Do Records support inheritance?
Records cannot extend other classes, but they can implement interfaces. Combine records with sealed interfaces to model closed type hierarchies.
Are Records immutable?
Records are shallowly immutable: their components are private final. If a component is itself a mutable object such as ArrayList, the record protects the reference but not the contents.
How do Records work with Jackson?
Jackson 2.12 and later support records natively. Serialization uses the synthesized accessors; deserialization uses the canonical constructor.
Can I use Bean Validation on Records?
Yes. Place constraint annotations such as @NotNull, @Email, and @Size directly on record components. The Jakarta Validation 3.0 specification supports records.
Do Records work with MapStruct?
Yes. MapStruct 1.5 and later support records as both source and target types and use the canonical constructor when building target records.
How does Spring Boot handle Records?
Spring Boot 3 supports records for @RequestBody, @ResponseBody, @ConfigurationProperties, @ModelAttribute, and most binding scenarios. Spring Data JDBC supports records as aggregate roots; Spring Data JPA does not.
Can a Record have a default no-arg constructor?
No. The canonical constructor is the only way to build a record. You can, however, declare additional constructors that ultimately delegate to the canonical one.
Are Records value types?
Records use value-based equality and hash codes, but they are still reference types in Java today. Project Valhalla aims to introduce true value types that records may eventually adopt.
How much memory does a Record use?
A record uses roughly the same memory as an equivalent POJO: the object header (12 or 16 bytes depending on compressed oops) plus the sum of its component sizes, padded to an 8-byte boundary.
Can a Record be serialized?
Yes. Records implement Serializable if you declare it. Their serialization format is stable and based on their components, so they evolve more predictably than POJOs with custom writeObject methods.
Can I convert a POJO to a Record automatically?
Yes, this tool does exactly that. It detects POJO fields, generates the equivalent record declaration, and warns you when a field is incompatible (for example, a public mutable list).
Should I use Records for every DTO?
For new code, default to records and switch to POJOs only when a specific framework requirement demands mutability. Existing POJOs do not need a rewrite, but new DTOs should be records.
Does this tool send my code to a server?
No. All conversion runs entirely in your browser. Nothing is uploaded, logged, or stored on our servers.