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 "";
}
}Java Records: Boilerplate-Free Immutable Data Carrier Classes
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:
A record can represent the same basic data model much more concisely:
public record User(String name, int age) {
}The record declaration communicates the important information immediately:
User
├── name
└── ageWhy 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:
record RecordName(Type field1, Type field2) {
}For example:
public record Product( String name, double price) {
}You can create an instance:
Product product = new Product("Laptop", 75000.0);Access the components using accessor methods:
System.out.println(product.name());
System.out.println(product.price());Notice that record accessors do not use the traditional JavaBean get prefix.
You write:
product.name()rather than:
product.getName()What Does a Record Automatically Provide?
For a record such as:
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:
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:
public record Employee( String name, int salary) {
}the generated accessors are:
employee.name();
employee.salary();Example:
Employee employee = new Employee("Tanmoy", 80000);
System.out.println(employee.name());
System.out.println(employee.salary());Output:
Tanmoy80000There is no need to manually write:
getName()getSalary()Record Constructor
A record automatically receives a canonical constructor corresponding to its components.
For:
public record Point(int x, int y) {
}you can write:
Point point = new Point(10, 20);The constructor receives:
x = 10y = 20and 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:
public record User(String name, int age) {
}You cannot do something like:
user.name = "Riya";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:
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.
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.RecordCannot explicitly extend another class
Are implicitly final
Have record components
Provide generated accessors
Provide generated value-oriented
equals(),hashCode(), andtoString()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:
public record Employee( long id, String name, String department) {
}Create an object:
Employee employee = new Employee( 101, "Tanmoy", "Development");Read the values:
System.out.println(employee.id());
System.out.println(employee.name());
System.out.println(employee.department());Output:
101TanmoyDevelopmentequals() with Records
Records automatically provide an equals() implementation based on their components.
Example:
public record Point(int x, int y) {
}Now:
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
System.out.println(p1.equals(p2));Output:
trueThe 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:
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:
public record User( String name, int age) {
}this:
User user = new User("Tanmoy", 35);
System.out.println(user);produces output similar to:
User[name=Tanmoy, age=35]This is useful for debugging and logging.
Custom Constructor Validation
Records can contain validation logic.
For example:
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:
User user = new User("Tanmoy", 35);works normally.
But:
User user = new User("Tanmoy", -1);throws:
IllegalArgumentExceptionCompact Canonical Constructor
The compact constructor syntax is:
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.
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:
public record Rectangle( double width, double height) {
public double area() {
return width * height;
}
}Use it:
Rectangle rectangle = new Rectangle(10, 5);
System.out.println(rectangle.area());Output:
50.0Records are therefore not limited to automatically generated methods.
Static Methods in Records
Records can also contain static methods.
public record Temperature(double celsius) {
public static Temperature fromFahrenheit( double fahrenheit) {
double celsius =(fahrenheit - 32) * 5 / 9;
return new Temperature(celsius);
}
}Use it:
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:
interface Printable {
void print();
}A record can implement it:
public record User( String name, int age) implements Printable {
@Override public void print() {
System.out.println( name + " - " + age);
}
}Then:
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:
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:
public record User( String name) {
}You cannot create:
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:
public record User(String name) {
public static final String TYPE = "APPLICATION_USER";
public String greeting() {
return "Hello " + name;
}
}Use:
System.out.println(User.TYPE);
User user = new User("Tanmoy");
System.out.println(user.greeting());Record Components vs Fields
When you write:
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:
user.name();
user.age();rather than directly accessing fields.
Records and Getters
Traditional JavaBean classes commonly use:
user.getName();
user.getAge();Records use:
user.name();
user.age();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:
public record UserResponse( long id, String name, String email) {
}A controller or service can return:
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:
{
"id": 101, "name": "Tanmoy", "status": "ACTIVE"
}A record can represent the response:
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:
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:
record Point(int x, int y) {
}modern Java can use a record pattern such as:
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:
public record Product( String name, double price) {
}can represent structured JSON data such as:
{
"name": "Laptop", "price": 75000
}Always verify the specific serialization framework and version when designing production APIs.
Record vs Traditional Class
| Feature | Traditional Class | Record |
|---|---|---|
| Boilerplate | Often higher | Low |
| Accessors | Usually written manually | Generated |
| Constructor | Usually written manually | Generated |
equals() | Usually implemented manually | Generated |
hashCode() | Usually implemented manually | Generated |
toString() | Usually implemented manually | Generated |
| Subclassing | Possible unless final | Not possible |
| Extends another class | Yes | No |
| Implements interfaces | Yes | Yes |
| Main purpose | General object modeling | Data-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:
public class User {
private String name;
public void setName(String name) {
this.name = name;
}
}The object can change after construction.
A record:
public record User(String name) {
}is designed around fixed component values established during construction.
Therefore:
POJO→ General-purpose objectRecord→ Data-oriented value carrierWhen 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:
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:
user.setName("Riya");Records do not automatically provide setters.
Instead, create another record instance:
User updated = new User("Riya", user.age());Mistake 2: Assuming Deep Immutability
Consider:
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:
record User(String name) {
}the accessor is:
user.name();not:
user.getName();Complete Java Record Example
Here is a practical example combining validation, methods, and generated behavior:
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:
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.
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:
record User(String name) {
}the accessor is:
user.name();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
Traditional Class
├── Fields
├── Constructor
├── Getters
├── equals()
├── hashCode()
└── toString()
↓
RECORD
┌─────────────────────────┐
│ record User(name, age) │
└─────────────────────────┘
↓
Boilerplate ReducedGraphic 2 — Java Record Structure
Java Record
|
┌───────┴───────┐
↓
↓
Components Generated API
| |
name, age constructor
name()
age()
equals()
hashCode()
toString()Graphic 3 — Record Immutability
new User("Tanmoy", 35)↓Record Object↓Component references cannot be reassigned through normal settersNote:Referenced mutableobjects may still change.Graphic 4 — Record Use Cases
Java Records
|
┌───┼────┬──────┐
↓
↓
↓
↓
DTO API Query Config
Results DataRunnable Code Example: Java Records
Java 17+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
}
}