What are Records in Java and when should you use them?

Java Records Interview Question

1. Short Answer

A Java Record (stable since Java 16, JEP 395) is a concise, immutable data carrier class. Declaring:

record Point(int x, int y) {}

automatically provides a canonical constructor, accessor methods x() and y(), and correct implementations of equals(), hashCode(), and toString() — eliminating the boilerplate you would otherwise write manually or via Lombok.

2. What is a Java Record?

Before Java 16, creating a simple immutable value object (e.g., a 2D point) required significant boilerplate:

// Pre-Record Java — lots of boilerplate
public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

With Records, all of the above collapses to a single line:

record Point(int x, int y) {}
Key Concept

Records are not just syntactic sugar. They carry semantic meaning: a record declares that its primary purpose is holding data, and its representation IS its API. The compiler enforces this by making record components final.

3. Auto-Generated Members

For a record declared as record Person(String name, int age) {}, Java automatically generates:

Member What is generated
Canonical constructor Person(String name, int age)
Accessor methods name() and age() — NOT getName()
equals() Compares all components by value
hashCode() Based on all components
toString() Person[name=Alice, age=30]
Accessor Naming

Record accessors use the component name directly: person.name(), not person.getName(). This can break compatibility with frameworks expecting JavaBean conventions. Keep this in mind when using records with older libraries.

4. Complete Code Example

// Java 16+ — Record declaration
record Person(String name, int age) {}

// Java 17+ — Record with custom method
record Product(String id, String name, double price) {

    // Custom instance method
    public String displayLabel() {
        return name + " ($" + String.format("%.2f", price) + ")";
    }

    // Static factory method
    public static Product free(String id, String name) {
        return new Product(id, name, 0.0);
    }
}

public class RecordDemo {
    public static void main(String[] args) {
        Person alice = new Person("Alice", 30);
        Person alice2 = new Person("Alice", 30);

        // Accessor methods
        System.out.println(alice.name());   // Alice
        System.out.println(alice.age());    // 30

        // equals() by value
        System.out.println(alice.equals(alice2)); // true

        // toString()
        System.out.println(alice); // Person[name=Alice, age=30]

        // Custom method
        Product p = new Product("P001", "Java Book", 49.99);
        System.out.println(p.displayLabel()); // Java Book ($49.99)

        // Static factory
        Product freeItem = Product.free("P002", "Trial License");
        System.out.println(freeItem.price()); // 0.0
    }
}

5. Compact Constructors — Adding Validation

A compact constructor lets you add validation or normalization without repeating the parameter list. The assignment to fields happens automatically after the compact constructor body runs.

record Range(int min, int max) {

    // Compact constructor — no parameter list, no this.field = field
    Range {
        if (min > max) {
            throw new IllegalArgumentException(
                "min (%d) must be <= max (%d)".formatted(min, max)
            );
        }
        // Can also normalize: min = Math.min(min, max);
    }
}

// Usage
Range valid = new Range(1, 10);   // OK
Range bad   = new Range(10, 1);   // throws IllegalArgumentException
Pro Tip

Use compact constructors for input validation and normalization. You can even reassign the parameters inside the compact constructor body — the modified values are what get stored in the final fields.

6. Records vs Lombok @Value

Both produce immutable value objects. Here is how they compare:

Feature Java Record Lombok @Value
Dependency None — built into JDK 16+ Requires Lombok on classpath
Accessor style name() getName() (JavaBean)
Inheritance Cannot extend/be extended Can extend classes
Validation Compact constructor Custom constructor body
Pattern matching Native support (Java 21) Not supported
Serialization Native JVM support Works via annotation
JPA entity Not suitable Not suitable either
// Lombok @Value equivalent
@Value
public class PersonLombok {
    String name;
    int age;
    // generates: getName(), getAge(), equals, hashCode, toString
}

// Java Record equivalent
record PersonRecord(String name, int age) {}
// generates: name(), age(), equals, hashCode, toString

7. When NOT to Use Records

Records are not a universal replacement for classes. Avoid them when:

  • Mutable state required — record components are implicitly final. You cannot write a setter.
  • Inheritance needed — records implicitly extend java.lang.Record and are final. They cannot extend any other class.
  • JPA / Hibernate entities — these require a no-arg constructor, mutable fields, and JavaBean accessor naming.
  • Framework compatibility — Jackson (older versions), MapStruct, and some Spring components expect getX() naming. Configure the library or use a different approach.
  • Optional fields — all record components are required. You cannot have optional components without custom constructor overloads.
Jackson Compatibility

Jackson 2.12+ supports records natively. Older versions require @JsonProperty annotations or explicit constructor configuration. Spring Boot 2.5+ ships with Jackson 2.12+, so most Spring projects handle records automatically.

8. Interview Context

Interviewers ask about Records to gauge your knowledge of modern Java. Strong answers will:

  • Mention Java 16 (stable) and the problem Records solve (boilerplate in value objects)
  • List the five auto-generated members
  • Explain the x() vs getX() naming difference
  • Note that records are implicitly final and extend java.lang.Record
  • Discuss compact constructors for validation
  • Know when NOT to use them (JPA entities, mutable objects)
Bonus Points

Mention that records are a natural companion to sealed classes and pattern matching switch (Java 21), enabling algebraic data type modeling similar to Kotlin data classes or Scala case classes.

9. Common Pitfalls

  • Expecting getters to be named getX() — they are named x(). Code or frameworks relying on JavaBean convention will break.
  • Trying to add instance fields — records only allow static fields beyond their components. Attempting to add private int z; is a compile error.
  • Forgetting records are final — you cannot subclass a record, nor can a record extend another class.
  • Using records as JPA entities — JPA requires a no-arg constructor and mutable state. Use a regular class for entities.
  • Implementing equals() manually — the generated equals() compares all components. If you override it, make sure you override hashCode() too.

10. Conclusion

Java Records are one of the most developer-friendly additions to modern Java. They eliminate boilerplate for immutable data carriers, integrate with the type system, and compose naturally with sealed classes and pattern matching. Use them confidently for DTOs, domain value objects, API response types, and any place you previously reached for Lombok @Value — provided the project runs Java 16+ and the accessors' naming convention is acceptable to your ecosystem.