Java Records vs Regular DTO Classes: When to Use What?

Arvind Kumar
4 min readJan 28, 2025

--

In modern Java applications, Data Transfer Objects (DTOs) are commonly used to carry data between layers such as controllers, services, and persistence layers. Traditionally, developers have relied on plain old Java objects (POJOs) with getters, setters, and constructors to represent DTOs. However, with the introduction of Java Records in Java 14 (as a preview) and officially in Java 16, developers now have a more concise and immutable way to define DTOs.

But should you replace all your DTOs with records? The answer depends on the use case.

In this article, we’ll explore 5 scenarios where records are a perfect fit and 5 cases where traditional DTOs are still the better choice.

What Are Java Records?

Java records are a special type of class designed to serve as immutable data carriers. They eliminate the boilerplate code associated with traditional DTOs by automatically generating:

  • A constructor
  • Getters (without the “get” prefix)
  • equals(), hashCode(), and toString() implementations

Example of a Java Record:

public record UserDTO(String name, int age, String email) {}

This single-line definition is equivalent to the following verbose POJO:

public class UserDTO {
private final String name;
private final int age;
private final String email;

public UserDTO(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String name() { return name; }
public int age() { return age; }
public String email() { return email; }
@Override
public boolean equals(Object o) { /* auto-generated */ }
@Override
public int hashCode() { /* auto-generated */ }
@Override
public String toString() { /* auto-generated */ }
}

When to Use Java Records Instead of Regular DTO Classes

Java records shine in situations where immutability, simplicity, and readability are important. Below are five key scenarios where you should consider using records over traditional DTOs:

1. API Response Models

When returning data from RESTful APIs, immutability is often desired to ensure thread safety and data integrity. Records provide a clean and efficient way to represent API response objects.

Example:

public record UserResponse(String name, int age, String email) {}

Why use records?

  • Reduces boilerplate code
  • Provides thread-safe, immutable responses

2. Configuration Data Holders

If you need to store static application configuration values that shouldn’t change once set, records are an excellent choice.

Example:

public record AppConfig(String appName, int maxUsers) {}

Why use records?

  • Immutability prevents accidental modifications
  • Easier to manage fixed application settings

3. Key-Value Pair Representations (e.g., Caching, Maps)

Records work well for storing key-value pairs in caching mechanisms or mapping operations where simple, read-only objects are sufficient.

Example:

public record CacheEntry(String key, Object value) {}

Why use records?

  • Simplifies working with map-based storage
  • Reduces overhead in frequently accessed objects

4. Database Query Results (Projection)

When working with relational databases, sometimes you need only a subset of fields rather than the entire entity. Records make it easy to create lightweight, immutable projections.

Example:

public record ProductSummary(String name, double price) {}

Why use records?

  • Faster object creation for read-heavy queries
  • Prevents unintended changes to query results

5. Event-Driven Systems (Message Payloads)

In event-driven architectures, events should be immutable and easily serializable. Records naturally align with this requirement.

Example:

public record OrderEvent(String orderId, String status, Instant timestamp) {}

Why use records?

  • Ensures event integrity across distributed systems
  • Easy serialization and logging

When NOT to Use Java Records

Despite their advantages, records are not suitable for all scenarios. Below are five situations where regular DTO classes are the better choice:

1. Mutable Objects Required

If you need to update object fields over time, records are not suitable because they are immutable by design.

Example:

class UserProfile {
private String name;
private int age;
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
}

When to avoid records?

  • If objects require frequent updates after creation
  • Scenarios involving complex workflows

2. Complex Business Logic Encapsulation

When DTOs require additional methods with complex logic, validation rules, or computed fields, traditional classes provide more flexibility.

Example:

class CustomerDTO {
private String name;
private double balance;
public double calculateDiscount() { return balance > 1000 ? 10 : 5; }
}

When to avoid records?

  • Need to encapsulate business logic inside the DTO
  • Data transformations require internal state tracking

3. Inheritance and Hierarchical Structures

Records do not support inheritance, so if your application relies on polymorphism, abstract classes, or extending functionality, traditional DTOs are necessary.

Example:

abstract class PersonDTO {
protected String name;
public abstract void display();
}

class EmployeeDTO extends PersonDTO {
@Override
public void display() { System.out.println("Employee: " + name); }
}

When to avoid records?

  • Inheritance-based design patterns
  • Common functionality shared across multiple DTOs

4. Framework Requirements (e.g., JPA Entities)

Many frameworks like Hibernate/JPA require no-arg constructors and mutable fields for entity persistence, making records incompatible.

Example:

@Entity
class ProductEntity {
@Id
@GeneratedValue
private Long id;
private String name;
}

When to avoid records?

  • When working with ORM frameworks like JPA
  • If frameworks require proxy-based lazy loading

5. Interoperability with JavaBeans-Conformant Libraries

Certain libraries expect DTOs to follow the JavaBeans pattern with getter/setter conventions, which records do not provide.

Example:

class OrderDTO {
private String orderId;
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
}

When to avoid records?

  • When using libraries relying on reflection-based data binding
  • Compatibility with older frameworks

Conclusion

Java records are a powerful feature that simplifies the creation of immutable data carriers, making them ideal for API responses, configurations, and event-driven systems. However, traditional DTO classes still hold their ground in scenarios requiring mutability, inheritance, and framework compatibility.

Use records when:

  • You need immutability, simplicity, and concise code.

Stick to traditional DTOs when:

  • Your application requires mutable state, complex logic, or framework support.

— — — —

Please share your thoughts/feedback in the comments!

--

--

Arvind Kumar
Arvind Kumar

Written by Arvind Kumar

Staff Engineer @Chegg || Passionate about technology || https://youtube.com/@codefarm0

Responses (4)