Record types for immutable data
package com.example.demo.dto;
import java.time.LocalDateTime;
import java.util.List;
// Simple record
public record UserDTO(
Long id,
String name,
String email
) {}
// Record with validation
public record CreateUserRequest(
String name,
String email,
String password
) {
// Compact constructor for validation
public CreateUserRequest {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Password must be at least 8 characters");
}
}
}
// Record with computed fields
public record Money(double amount, String currency) {
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount + other.amount, currency);
}
public String formatted() {
return String.format("%s %.2f", currency, amount);
}
}
// Record implementing interface
public interface Identifiable {
Long getId();
}
public record Post(
Long id,
Long userId,
String title,
String content,
LocalDateTime createdAt
) implements Identifiable {
@Override
public Long getId() {
return id;
}
public boolean isRecent() {
return createdAt.isAfter(LocalDateTime.now().minusDays(7));
}
}
// Nested records
public record Order(
Long id,
Customer customer,
List<LineItem> items,
Money total
) {
public record Customer(String name, String email) {}
public record LineItem(String product, int quantity, Money price) {}
}
// Record with static factory method
public record ApiResponse<T>(
boolean success,
T data,
String message,
int statusCode
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, "Success", 200);
}
public static <T> ApiResponse<T> error(String message, int statusCode) {
return new ApiResponse<>(false, null, message, statusCode);
}
}
package com.example.demo.controller;
import com.example.demo.dto.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class RecordController {
private final UserService userService;
public RecordController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ApiResponse<UserDTO> createUser(@RequestBody CreateUserRequest request) {
try {
UserDTO user = userService.createUser(request);
return ApiResponse.success(user);
} catch (Exception e) {
return ApiResponse.error(e.getMessage(), 400);
}
}
@GetMapping("/{id}")
public ApiResponse<UserDTO> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ApiResponse::success)
.orElse(ApiResponse.error("User not found", 404));
}
}
Java Records (JDK 14+) provide concise syntax for immutable data carriers. Records automatically generate constructor, getters, equals, hashCode, and toString. I use records for DTOs, value objects, and API responses. Records are final and all fields are final by default. Compact constructor syntax validates or normalizes data. Records can implement interfaces and have static methods. They work perfectly with pattern matching and sealed types. Serialization libraries like Jackson support records natively. Records reduce boilerplate compared to traditional classes, improving code clarity. They're ideal for representing pure data without behavior. Spring Boot integrates records smoothly for request/response handling. Records modernize Java code, making it more expressive and maintainable.