Java Records (Java 16+) — Concise Immutable Data Carriers
Fast recall
Quick revision
Core answer
record User(Long id, String name, String email) {} and the compiler generates: a canonical constructor taking all fields, public accessor methods named after each field (id(), name(), email() — not getId()), equals and hashCode based on all fields, and a toString showing all fields. Records are implicitly final, all fields are final, and they implicitly extend `java.lang.Record` (so they can't extend any other class — but they can implement interfaces). For validation or normalization, use a compact constructor — public Money { Objects.requireNonNull(amount); ... } — which runs after the parameters are set, before assignment. You can add static factories, custom methods, nested records, and even a non-canonical constructor that delegates to the canonical one. Best uses: DTOs (request/response models), value objects (Money, Coordinate, DateRange), domain events, method return types for grouping multiple values. Limitations: not for JPA `@Entity` (JPA needs a no-arg constructor and mutable state — records have neither), not for objects with identity (use a class), no inheritance (compose instead). Compared to Lombok @Value/@Data: records are part of the JDK (no annotation processor), are recognized natively by tools, deconstruct in switch patterns (Java 21), and require less compile-time magic. Records also pair perfectly with sealed types (Java 17) for ADT-style modeling — sealed interface Shape permits Circle, Square with each subtype as a record.Whether the candidate knows record syntax (canonical constructor, accessors), compact constructor for validation, the implicit final/extends-Record rules, and when records are wrong (JPA entities, identity, mutability)
Calling `getName()` instead of `name()`. Trying to use a record as a JPA entity. Trying to extend another class with a record. Putting mutable state inside a record (defeats the purpose)
Mention sealed types + records for ADTs, deconstruction patterns in Java 21 switch, comparison with Lombok `@Value` (records win for portability), and patterns like compact constructor normalization
Interview-ready guide
Interview answer
Records (Java 16, JEP 395) are a concise way to declare immutable data carrier classes. One line: record User(Long id, String name, String email) {} and the compiler generates everything.
What the compiler generates
- Canonical constructor taking all fields
- Public accessor methods named after each field —
id(),name()(NOTgetId()) equalsandhashCodebased on all componentstoStringshowing all fields
Implicit rules
- Records are
final— can't be subclassed - Records implicitly extend
java.lang.Record— can't extend other classes - Can implement interfaces
- All fields are
private final
Compact constructor for validation/normalization
public Money { Objects.requireNonNull(amount); currency = currency.toUpperCase(); }- No parameter list — they're already in scope
- No
this.x = x— assignment is automatic after the body - Reassigning the parameter normalizes the value (field gets the new value)
- Throw to fail validation
You can add
- Custom methods
- Static factories
- Nested records
- Static fields/methods
- Non-canonical constructors (must delegate via
this(...))
You CANNOT add
- Instance fields beyond components
- Mutable state
- Inheritance from other classes
Records pair perfectly with sealed types (Java 17) — ADTs
sealed interface Shape permits Circle, Squarewith each subtype as a record- Pattern matching switch (Java 21) deconstructs them
case Circle(double r) -> Math.PI * r * r- Compiler verifies exhaustiveness
- Same expressive power as Scala case classes or Rust enums
Best uses
- DTOs (request/response models)
- Value objects (Money, Coordinate, DateRange)
- Domain events
- Method return types for grouping multiple values
- Pattern matching with sealed types
When NOT to use records
1. JPA `@Entity`
- JPA needs no-arg constructor and mutable fields
- Records have neither
- Use a class for entities; map to records as DTOs
2. Identity-based objects
- A
Useridentified by ID, not all fields - Records auto-generate equals based on all fields — wrong
- Use a class with explicit equals based on ID
3. Mutable state
- Records have only final fields
- For mutable: use a class
- 'Wither' workaround:
withName(String n)returns a new record
4. Complex inheritance
- Can implement interfaces, can't extend classes
- For shared implementation: composition
5. Defensive copies for mutable components
- Records hold references; List/Map components are still mutable through the reference
- Use
List.copyOf(tags)in compact constructor for true immutability
Records vs Lombok `@Value`
- Records: native JDK, no annotation processor, work in IDEs without plugins
- Records: recognized by Jackson, Spring, Bean Validation natively
- Records: deconstruct in pattern matching switch (Java 21)
- Lombok still useful for
@Builder, mutable@Data,@Slf4j— records don't replace those - For new code: prefer records for pure immutable data
Records with Jackson
- Jackson 2.12+ recognizes records natively
- Deserializes via canonical constructor
- Field name matches JSON property by default
Records with Bean Validation
- Annotate components:
record CreateRequest(@NotBlank String name, @Email String email) {} - Works with Jakarta Validation 3.0+
Records with Spring Data — query projections
- DTO projections via
SELECT new com.example.UserSummary(u.id, u.name) ... - Interface-based projections also work
Records in Maps and Sets
- Work perfectly —
equals/hashCodebased on all components - Useful as composite keys
Style recommendations
- DTOs, value objects, return-grouping, events: records
- Entities, services, mutable state, identity: classes
- Compact constructor for validation
- Defensive copy collection components
- Don't add many custom methods to records — if you need behavior, consider a class
- Single-component records are sometimes overkill — use the underlying type
Recommendation: Use records as your default for any immutable data carrier — DTOs, value objects, domain events, method return types that need to group multiple values. The 1-line declaration replaces 20+ lines of class boilerplate. Use a compact constructor for validation and normalization — fail fast on invalid input, normalize values (uppercase, strip whitespace, defensive copy collections). Pair records with sealed interfaces (Java 17) for ADT-style domain modeling — each variant is a record, the sealed type is the closed hierarchy, and pattern matching switch (Java 21) gives you compile-time exhaustive matching with deconstruction. Don't use records for JPA entities (need mutable state and no-arg constructor), identity-based objects (auto-generated equals is wrong for ID-based identity), or mutable state. Don't try to extend classes — records can only implement interfaces. Records win over Lombok `@Value` for new code: native to the JDK, no annotation processor, recognized by all tools, and they deconstruct in pattern matching. Lombok still has its place for @Builder and mutable @Data — they're not in competition for the same feature. The mental model: records are a class shorthand for immutable data with value semantics — nothing more, nothing less. They pair beautifully with sealed types, pattern matching, and the modern Java functional style.
Java Records (Java 16+) — Concise Immutable Data Carriers
Direct Answer
Records (Java 16, JEP 395) are a concise way to declare immutable data carrier classes. One line: record User(Long id, String name, String email) {} and the compiler generates: a canonical constructor taking all fields, public accessor methods named after each field (id(), name(), email() — not getId()), equals and hashCode based on all fields, and a toString showing all fields. Records are implicitly final, all fields are final, and they implicitly extend java.lang.Record (so they can't extend any other class — but they can implement interfaces). For validation or normalization, use a compact constructor — public Money { Objects.requireNonNull(amount); ... } — which runs after the parameters are set, before assignment. You can add static factories, custom methods, nested records, and even a non-canonical constructor that delegates to the canonical one. Best uses: DTOs (request/response models), value objects (Money, Coordinate, DateRange), domain events, method return types for grouping multiple values. Limitations: not for JPA @Entity (JPA needs a no-arg constructor and mutable state — records have neither), not for objects with identity (use a class), no inheritance (compose instead). Compared to Lombok @Value/@Data: records are part of the JDK (no annotation processor), are recognized natively by tools, deconstruct in switch patterns (Java 21), and require less compile-time magic. Records also pair perfectly with sealed types (Java 17) for ADT-style modeling — sealed interface Shape permits Circle, Square with each subtype as a record.
Common Mistakes & Pitfalls
Calling getName() instead of name(). Trying to use a record as a JPA entity. Trying to extend another class with a record. Putting mutable state inside a record (defeats the purpose)
Check your understanding
Answer these without looking back at the article.
- Why can't records be JPA entities?
- What's the difference between a compact constructor and a canonical constructor?
- How do records pair with sealed types for ADT-style modeling?
- Why are accessors called `name()` instead of `getName()`?
- When would you choose a class over a record?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.