How to Use Java Records with Spring Boot

 Introduction to Java record

Java 14 introduced the Java record feature (as a preview) and officially introduced it in Java 16. A Java record is a special class used to store immutable data.

📌 Key Features of Java record:
Immutable fields – Cannot be modified after creation.
Auto-generated constructor, getters, toString(), equals(), and hashCode().
Ideal for DTOs, API responses, and data modeling.

Declaring a Simple record (Employee Example)

public record Employee(int id, String firstName, String lastName, String email) {}

📌 What this automatically generates:

// Equivalent traditional Java class
public final class Employee {
private final int id;
private final String firstName;
private final String lastName;
private final String email;

public Employee(int id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}

public int id() { return id; }
public String firstName() { return firstName; }
public String lastName() { return lastName; }
public String email() { return email; }

@Override
public boolean equals(Object obj) { /* Auto-generated */ }
@Override
public int hashCode() { /* Auto-generated */ }
@Override
public String toString() { /* Auto-generated */ }
}

📌 Why record is better?
✅ No need to write constructors, getters, toString(), equals(), or hashCode().
Immutable by default (No setters).

🔥 Why Use Records in Spring Boot?

✅ Common Use Cases in Spring Boot

Here are the best places to use records in a Spring Boot app:

Real-World Example: REST API with Java Records

Let’s build a simple Spring Boot app with a Product entity and a ProductDTO using record.

Step 1: Entity Class

@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private String description;
private Double price;

// Getters, Setters, Constructors (or use Lombok)
}

Step 2: DTO Using Record

public record ProductDTO(Long id, String name, String description, Double price) {}

This generates:

  • A constructor with all fields
  • getters (named as the fields)
  • equals(), hashCode(), and toString()
🧠 Records are implicitly final and immutable.

✅ One line replaces 40+ lines of code!

Step 3: Mapper Utility

public class ProductMapper {

public static ProductDTO toDTO(Product product) {
return new ProductDTO(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice()
);
}

public static Product toEntity(ProductDTO dto) {
Product product = new Product();
product.setName(dto.name());
product.setDescription(dto.description());
product.setPrice(dto.price());
return product;
}
}

Step 4: Repository Layer

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Step 5: Service Layer

@Service
public class ProductService {

private final ProductRepository repository;

public ProductService(ProductRepository repository) {
this.repository = repository;
}

public List<ProductDTO> getAllProducts() {
return repository.findAll()
.stream()
.map(ProductMapper::toDTO)
.toList();
}

public ProductDTO createProduct(ProductDTO dto) {
Product product = ProductMapper.toEntity(dto);
Product saved = repository.save(product);
return ProductMapper.toDTO(saved);
}
}

Step 6: REST Controller

@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService service;

public ProductController(ProductService service) {
this.service = service;
}

@GetMapping
public List<ProductDTO> getAllProducts() {
return service.getAllProducts();
}

@PostMapping
public ProductDTO createProduct(@RequestBody ProductDTO dto) {
return service.createProduct(dto);
}
}

💡 Using Records for Custom API Responses

public record ApiResponse<T>(String message, T data, LocalDateTime timestamp) {}

Example Usage:

@GetMapping
public ApiResponse<List<ProductDTO>> getAll() {
return new ApiResponse<>("Products fetched", service.getAllProducts(), LocalDateTime.now());
}

✅ Looks great, minimal code, and makes the API more structured.

🚀 Using Records with Spring Data Projections

You can return a record from a custom JPQL query:

public record ProductView(Long id, String name) {}

@Query("SELECT new com.example.demo.ProductView(p.id, p.name) FROM Product p")
List<ProductView> findAllProductNames();

⚠️ Things You CANNOT Do with Records

Best Practices

  1. ✅ Use records only when immutability makes sense (e.g., DTOs, value models).
  2. ❌ Don’t use records for JPA entities (JPA requires mutable fields).
  3. ✅ Combine records with @RequestBody or @ResponseBody for cleaner REST APIs.
  4. ✅ Use records for in-memory and read-only operations.
  5. ✅ Use records in unit tests for mock payloads and expected results.

Unit Test Example

@Test
void testProductDTO() {
ProductDTO dto = new ProductDTO(1L, "Book", "Java Book", 399.99);
assertEquals("Book", dto.name());
}

Frameworks and Libraries That Work Well with Records

📚 Summary

  • Java records are lightweight, immutable data carriers.
  • In Spring Boot, they’re perfect for DTOs, responses, and projections.
  • They reduce boilerplate, improve readability, and encourage immutability.
  • But avoid using them as entities or for mutable state.

🎯 Final Thoughts

Java Records bring the modern, minimalist syntax we’ve long wanted in Java. When combined with Spring Boot, they offer a clean, professional structure — especially for REST APIs, microservices, and cloud-native applications.

✅ Cleaner code
✅ Fewer bugs
✅ Easier maintenance

Still using POJOs for DTOs? It’s time to upgrade to Java Records and experience the simplicity.

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare