Topic 102 of 119 13. Java New Features Interactive Tutorial

Java Records: Boilerplate-Free Immutable Data Carrier Classes

9 mins read Last updated: 22 Aug 2026 32,646 views

Direct Answer & Core Concept

Java Records provide a concise way to create classes whose main purpose is to carry data. Introduced as a preview feature in Java 14 and finalized in Java 16, records remove much of the repetitive code normally required for simple data-carrier classes.

Before records, a class containing a few fields often required constructors, accessor methods, equals(), hashCode(), and toString() implementations. A record can generate these members automatically from a compact declaration.

Records are particularly useful for DTOs, API responses, configuration data, query results, and other objects where the primary purpose is to represent a fixed set of values.

Detailed Explanation & Mechanics

What Is a Java Record?

A record is a special kind of class designed to model data.

A traditional class might look like this:

Main.java
Run Online
public final class User {
    private final String name;
    private final int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    } public String getName() {
        return name;
    } public int getAge() {
        return age;
    } @Override public boolean equals(Object obj) {
        // implementation
        return true;
    } @Override public int hashCode() {
        // implementation
        return 0;
    } @Override public String toString() {
        // implementation
        return "";
    }
}

A record can represent the same basic data model much more concisely:

Main.java
Run Online
public record User(String name, int age) {
}

The record declaration communicates the important information immediately:

Architecture Diagram
User
 ├── name
 └── age

Why Were Records Introduced?

Many Java classes exist primarily to carry data from one part of an application to another.

For example:

  • API request objects

  • API response objects

  • Database query results

  • Configuration values

  • Coordinates

  • Product information

  • User information

  • Event data

Writing all the repetitive code for these classes can make the source file unnecessarily large.

Records address this problem by allowing developers to describe the data model directly.


Basic Record Syntax

The basic syntax is:

Main.java
Run Online
record RecordName(Type field1, Type field2) {
}

For example:

Main.java
Run Online
public record Product( String name, double price) {
}

You can create an instance:

Main.java
Run Online
Product product = new Product("Laptop", 75000.0);

Access the components using accessor methods:

Main.java
Run Online
System.out.println(product.name());
System.out.println(product.price());

Notice that record accessors do not use the traditional JavaBean get prefix.

You write:

rather than:


What Does a Record Automatically Provide?

For a record such as:

Main.java
Run Online
public record User(String name, int age) {
}

the Java compiler provides important members based on its components.

These include:

  • A canonical constructor

  • Accessor methods

  • equals()

  • hashCode()

  • toString()

Conceptually, the record provides behavior similar to:

Main.java
Run Online
public String name() {
    return name;
}public int age() {
    return age;
}

It also generates value-based implementations of equals(), hashCode(), and toString() based on the record components.


Record Accessor Methods

For:

Main.java
Run Online
public record Employee( String name, int salary) {
}

the generated accessors are:

Main.java
Run Online
employee.name();
employee.salary();

Example:

Main.java
Run Online
Employee employee = new Employee("Tanmoy", 80000);
System.out.println(employee.name());
System.out.println(employee.salary());

Output:

There is no need to manually write:


Record Constructor

A record automatically receives a canonical constructor corresponding to its components.

For:

Main.java
Run Online
public record Point(int x, int y) {
}

you can write:

Main.java
Run Online
Point point = new Point(10, 20);

The constructor receives:

and initializes the record components.


Records and final Fields

Record components are final in the sense that the record does not provide ordinary reassignment of its component fields after construction.

For example:

Main.java
Run Online
public record User(String name, int age) {
}

You cannot do something like:

Record instances are therefore well suited to immutable-style data modeling.

However, there is an important detail:

A record is shallowly immutable, not automatically deeply immutable.

For example:

Main.java
Run Online
public record User( String name, List<String> roles) {
}

The roles reference cannot be reassigned through the record, but the List itself may still be mutable.

Main.java
Run Online
user.roles().add("ADMIN");

Whether that is possible depends on the actual list supplied to the record.

If deep immutability is required, the referenced objects must also be made immutable or defensively copied.


Records Are Not Just Immutable POJOs

A record is more than a shortcut for writing getters and setters.

It is a distinct Java language construct with specific semantics.

Records:

  • Extend java.lang.Record

  • Cannot explicitly extend another class

  • Are implicitly final

  • Have record components

  • Provide generated accessors

  • Provide generated value-oriented equals(), hashCode(), and toString()

  • Can implement interfaces

  • Can contain methods and constructors

They are intended for classes whose primary purpose is to model data.


Record Example

Consider an employee record:

Main.java
Run Online
public record Employee( long id, String name, String department) {
}

Create an object:

Main.java
Run Online
Employee employee = new Employee( 101, "Tanmoy", "Development");

Read the values:

Main.java
Run Online
System.out.println(employee.id());
System.out.println(employee.name());
System.out.println(employee.department());

Output:


equals() with Records

Records automatically provide an equals() implementation based on their components.

Example:

Main.java
Run Online
public record Point(int x, int y) {
}

Now:

Main.java
Run Online
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
System.out.println(p1.equals(p2));

Output:

The two records contain the same component values, so they compare equal.

This is particularly useful for data-transfer objects and value-like data structures.


hashCode() with Records

Records also provide a corresponding hashCode() implementation based on their components.

For example:

Main.java
Run Online
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());

Equal records have equal hash codes, following Java's general equals() and hashCode() contract.

This makes records convenient for use in hash-based collections when their component values have suitable equality and hashing behavior.


toString() with Records

Records also provide a useful toString() implementation.

For:

Main.java
Run Online
public record User( String name, int age) {
}

this:

Main.java
Run Online
User user = new User("Tanmoy", 35);
System.out.println(user);

produces output similar to:

Main.java
Run Online
User[name=Tanmoy, age=35]

This is useful for debugging and logging.


Custom Constructor Validation

Records can contain validation logic.

For example:

Main.java
Run Online
public record User( String name, int age) {
    public User {
        if (age < 0) {
            throw new IllegalArgumentException( "Age cannot be negative");
        }
    }
}

This is called a compact canonical constructor.

You do not need to repeat the component parameters or assignment statements.

Now:

Main.java
Run Online
User user = new User("Tanmoy", 35);

works normally.

But:

Main.java
Run Online
User user = new User("Tanmoy", -1);

throws:


Compact Canonical Constructor

The compact constructor syntax is:

Main.java
Run Online
public record Product( String name, double price) {
    public Product {
        if (price < 0) {
            throw new IllegalArgumentException( "Price cannot be negative");
        }
    }
}

The compiler handles the normal component initialization.

This makes validation concise while keeping the record declaration readable.


Explicit Canonical Constructor

You can also write the canonical constructor explicitly.

Main.java
Run Online
public record Product( String name, double price) {
    public Product(String name, double price) {
        if (price < 0) {
            throw new IllegalArgumentException( "Price cannot be negative");
        } this.name = name;
        this.price = price;
    }
}

The compact form is often easier when the constructor mainly performs validation.


Custom Methods in Records

Records can contain methods.

For example:

Main.java
Run Online
public record Rectangle( double width, double height) {
    public double area() {
        return width * height;
    }
}

Use it:

Main.java
Run Online
Rectangle rectangle = new Rectangle(10, 5);
System.out.println(rectangle.area());

Output:

Records are therefore not limited to automatically generated methods.


Static Methods in Records

Records can also contain static methods.

Main.java
Run Online
public record Temperature(double celsius) {
    public static Temperature fromFahrenheit( double fahrenheit) {
        double celsius =(fahrenheit - 32) * 5 / 9;
        return new Temperature(celsius);
    }
}

Use it:

Main.java
Run Online
Temperature temperature = Temperature.fromFahrenheit(98.6);
System.out.println(temperature.celsius());

Records Can Implement Interfaces

A record cannot extend another class, but it can implement interfaces.

Example:

Main.java
Run Online
interface Printable {
    void print();
}

A record can implement it:

Main.java
Run Online
public record User( String name, int age) implements Printable {
    @Override public void print() {
        System.out.println( name + " - " + age);
    }
}

Then:

Main.java
Run Online
User user = new User("Tanmoy", 35);
user.print();

Records can therefore participate in normal interface-based designs.


Records Cannot Extend Another Class

Every record implicitly extends java.lang.Record.

Therefore, you cannot write:

Main.java
Run Online
public record Employee(...) extends Person {
}

This is not allowed.

A record can implement interfaces, but it cannot extend an arbitrary class.

This is one reason to choose records only when their data-carrier semantics fit the design.


Records Are Implicitly Final

A record cannot be subclassed.

For example:

Main.java
Run Online
public record User( String name) {
}

You cannot create:

Main.java
Run Online
class AdminUser extends User {
}

Records are implicitly final.

This fits their purpose as well-defined data carriers whose structure should not be changed through subclassing.


Static and Instance Members

Records can contain additional members such as:

  • Instance methods

  • Static methods

  • Static fields

  • Nested types

  • Constructors

  • Implemented interfaces

For example:

Main.java
Run Online
public record User(String name) {
    public static final String TYPE = "APPLICATION_USER";
    public String greeting() {
        return "Hello " + name;
    }
}

Use:

Main.java
Run Online
System.out.println(User.TYPE);
User user = new User("Tanmoy");
System.out.println(user.greeting());

Record Components vs Fields

When you write:

Main.java
Run Online
public record User( String name, int age) {
}

name and age are called record components.

The compiler creates the corresponding private final component fields and accessor methods as part of the record's semantics.

You normally access them through:

rather than directly accessing fields.


Records and Getters

Traditional JavaBean classes commonly use:

Main.java
Run Online
user.getName();
user.getAge();

Records use:

This difference matters when integrating records with libraries that expect JavaBean-style getters.

For example, a framework or library that specifically searches for getName() may require additional configuration or an alternative data class.


Records as DTOs

One of the most common uses of records is creating DTOs, or Data Transfer Objects.

For example:

Main.java
Run Online
public record UserResponse( long id, String name, String email) {
}

A controller or service can return:

Main.java
Run Online
return new UserResponse( user.getId(), user.getName(), user.getEmail());

This avoids creating a large amount of repetitive DTO code.


Records for API Responses

Suppose an API returns:

Main.java
Run Online
{
    "id": 101, "name": "Tanmoy", "status": "ACTIVE"
}

A record can represent the response:

Main.java
Run Online
public record UserResponse( long id, String name, String status) {
}

Modern Java frameworks can often work with records for serialization and deserialization, subject to the framework and configuration being used.


Records for Database Results

Records can also represent read-only query results.

For example:

Main.java
Run Online
public record UserSummary( long id, String name, String department) {
}

A database query can map its result into this compact data carrier.

This can make application code easier to understand because the record directly describes the returned data.


Records and Pattern Matching

Records also work with modern Java pattern-matching features.

For example, in Java versions that support record patterns, a record can be deconstructed during pattern matching.

Given:

Main.java
Run Online
record Point(int x, int y) {
}

modern Java can use a record pattern such as:

Main.java
Run Online
if (object instanceof Point(int x, int y)) {
    System.out.println(x + y);
}

This allows the record components to be extracted directly.

Record patterns are a later Java feature and should not be confused with the original Java 16 record release.


Records and Serialization

Records can be used with serialization libraries, but compatibility depends on the library and version.

For JSON-based applications, modern libraries can support records by using their canonical constructor and component information.

For example:

Main.java
Run Online
public record Product( String name, double price) {
}

can represent structured JSON data such as:

Main.java
Run Online
{
    "name": "Laptop", "price": 75000
}

Always verify the specific serialization framework and version when designing production APIs.


Record vs Traditional Class

FeatureTraditional ClassRecord
BoilerplateOften higherLow
AccessorsUsually written manuallyGenerated
ConstructorUsually written manuallyGenerated
equals()Usually implemented manuallyGenerated
hashCode()Usually implemented manuallyGenerated
toString()Usually implemented manuallyGenerated
SubclassingPossible unless finalNot possible
Extends another classYesNo
Implements interfacesYesYes
Main purposeGeneral object modelingData-oriented modeling

Record vs Lombok @Data

Before records became available, developers often used libraries such as Lombok to reduce boilerplate.

For example, Lombok's @Data can generate getters, setters, equals(), hashCode(), and toString().

Records take a different approach.

A record is a Java language feature with explicit data-carrier semantics and does not provide ordinary setters.

For immutable data models, a record can therefore be a natural choice when its restrictions match the application's requirements.


Record vs POJO

A traditional POJO can be highly flexible.

For example:

Main.java
Run Online
public class User {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
}

The object can change after construction.

A record:

Main.java
Run Online
public record User(String name) {
}

is designed around fixed component values established during construction.

Therefore:

Main.java
Run Online
POJO→ General-purpose objectRecord→ Data-oriented value carrier

When Should You Use Java Records?

Records are a good fit when:

  • The class primarily carries data

  • The data should not be reassigned after construction

  • You want value-based equality

  • You want less boilerplate

  • You need concise DTOs

  • You are modeling API responses

  • You are representing query results

  • You are representing configuration values

  • You do not need class inheritance

For example:

Main.java
Run Online
public record Coordinates( double latitude, double longitude) {
}

is a natural record candidate.


When Should You Not Use a Record?

A traditional class may be more appropriate when:

  • The object needs mutable state

  • You need to extend another class

  • Subclasses are part of the design

  • JavaBean-style setters are required

  • The class has complex identity or lifecycle behavior

  • The object is more than primarily a data carrier

  • You need inheritance-based customization

For example, a complex domain object with substantial mutable behavior may be better represented by a normal class.


Common Mistakes with Records

Mistake 1: Expecting Setters

This will not work:

Records do not automatically provide setters.

Instead, create another record instance:

Main.java
Run Online
User updated = new User("Riya", user.age());

Mistake 2: Assuming Deep Immutability

Consider:

Main.java
Run Online
public record Team( List<String> members) {
}

The record component reference is fixed, but the list may still be mutable.

For stronger immutability, create or accept an immutable copy where appropriate.


Mistake 3: Using Records for Every Class

Records are designed for data-centric types.

Do not use a record simply because it requires fewer lines of code.

The design should match the semantics of the object.


Mistake 4: Expecting getName()

For:

Main.java
Run Online
record User(String name) {
}

the accessor is:

not:


Complete Java Record Example

Here is a practical example combining validation, methods, and generated behavior:

Main.java
Run Online
public record Product( long id, String name, double price) {
    public Product {
        if (id <= 0) {
            throw new IllegalArgumentException( "ID must be positive");
        } if (name == null || name.isBlank()) {
            throw new IllegalArgumentException( "Name cannot be empty");
        } if (price < 0) {
            throw new IllegalArgumentException( "Price cannot be negative");
        }
    } public double priceWithTax(double taxRate) {
        return price +(price * taxRate / 100);
    }
}

Use it:

Main.java
Run Online
Product product = new Product( 101, "Laptop", 75000);
System.out.println(product.name());
System.out.println( product.priceWithTax(18));

This demonstrates how a record can remain concise while still containing domain-specific validation and behavior.


Frequently Asked Questions

What is a record in Java?

A record is a special Java class designed primarily to model data with less boilerplate.

When were Java records introduced?

Records were previewed in Java 14 and became a permanent Java language feature in Java 16.

Are Java records immutable?

Records provide final component fields and do not provide setters, but their immutability is shallow. Mutable objects referenced by components can still be changed.

Can a record extend a class?

No. A record implicitly extends java.lang.Record and cannot extend another class.

Can a record implement an interface?

Yes.

Main.java
Run Online
record User(String name) implements Printable {
}

Can records have methods?

Yes. Records can contain instance methods, static methods, and other permitted members.

Can records have constructors?

Yes. Records automatically have a canonical constructor, and you can customize it using either an explicit or compact canonical constructor.

Do records have getters?

Records have accessor methods named after their components.

For:

Main.java
Run Online
record User(String name) {
}

the accessor is:

not getName().

Can records be inherited?

No. Records are implicitly final and cannot be subclassed.

Are records good for DTOs?

Yes. DTOs are one of the common use cases for records because they often primarily carry data.

Can records contain mutable objects?

Yes. A record component can reference a mutable object such as List. The record itself does not automatically make that referenced object immutable.

Can records override methods?

Yes. You can provide your own implementations of methods such as toString(), subject to the normal rules for record members. The accessors can also be explicitly declared when needed, but they must follow the record's required signature.

Suggested Graphics

Graphic 1 — Traditional Class vs Record

Architecture Diagram
Traditional Class
 ├── Fields
 ├── Constructor
 ├── Getters
 ├── equals()
 ├── hashCode()
 └── toString()
↓
RECORD

 ┌─────────────────────────┐
 │ record User(name, age)  │
 └─────────────────────────┘
↓
Boilerplate Reduced

Graphic 2 — Java Record Structure

Architecture Diagram
          Java Record
              |
      ┌───────┴───────┐
↓

↓
Components      Generated API
      |               |
 name, age       constructor
                 name()
                 age()
                 equals()
                 hashCode()
                 toString()

Graphic 3 — Record Immutability

Main.java
Run Online
new User("Tanmoy", 35)↓Record Object↓Component references cannot be reassigned through normal settersNote:Referenced mutableobjects may still change.

Graphic 4 — Record Use Cases

Architecture Diagram
Java Records
     |
 ┌───┼────┬──────┐
↓

↓

↓

↓
DTO API  Query  Config
       Results   Data

Runnable Code Example: Java Records

Java 17+
Main.java
Run Online
public class RecordDemo {
    // Single-line immutable data carrier class
    public record UserProfile(String username, String email, int loyaltyPoints) {
        // Compact constructor with validation
        public UserProfile {
            if (loyaltyPoints < 0) {
                throw new IllegalArgumentException("Loyalty points cannot be negative");
            }
        }
    } public static void main(String[] args) {
        UserProfile user = new UserProfile("sarah", "sarah@example.com", 350);
        System.out.println(user);
        // Auto-generated toString
        System.out.println("Username: " + user.username());
        // Accessor method
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

Can a Java Record extend another class?

No, all records implicitly extend java.lang.Record, and Java does not support multiple class inheritance.
Q2

How are record accessor methods named?

They match field names directly (e.g. user.username()) without the get prefix.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Use Java Records for DTOs (Data Transfer Objects), API responses, message payloads, and immutable composite map keys.