Spring Boot 3 + Record-Based DTOs: Cleaner APIs with Better Type Safety
Java 16 introduced records, a new kind of class designed to model immutable data. With the advent of Spring Boot 3, record-based DTOs (Data Transfer Objects) are not only supported but also encouraged for building cleaner, more secure, and type-safe APIs.
In this article, we’ll explore how to effectively use Java records as DTOs in Spring Boot 3 for request and response bodies, along with best practices to ensure security and validation.
1. Why Use Java Records as DTOs?
Records eliminate boilerplate code like constructors, getters, equals(), hashCode(), and toString(), making DTOs more concise and expressive.
✅ Benefits:
- Immutability by default
- Better readability
- Automatic component generation
- Safer data flow in APIs
- Natural fit for functional programming patterns
2. Basic Setup with Spring Boot 3
Add the dependencies in build.gradle or pom.xml:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
}
3. Using Record as Request DTO
public record CreateUserRequest(
@NotBlank String username,
@Email String email,
@Size(min = 6) String password
) {}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody CreateUserRequest request) {
// Save to DB or process
return ResponseEntity.ok("User created: " + request.username());
}
}
✅ Spring will automatically validate the input using annotations like @NotBlank, @Email, and @Size.
4. Using Record as Response DTO
public record UserResponse(
Long id,
String username,
String email
) {}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
UserResponse response = new UserResponse(id, "john_doe", "john@example.com");
return ResponseEntity.ok(response);
}
5. Security Considerations
- Avoid using entities directly in records
Don’t expose JPA entities via record-based DTOs — always use mappers or constructors to decouple layers. - Validate all input
Use@Validand@Validatedto enforce constraints at the controller level. - Sanitize sensitive data
Never expose passwords, tokens, or sensitive fields in response DTOs. - Use Jackson Mixins if necessary
Customize serialization without altering the record structure.
6. Example: User Registration Flow
Request DTO
public record RegisterRequest(
@NotBlank String name,
@Email String email,
@Size(min = 8) String password
) {}
Response DTO
public record RegisterResponse(
Long id,
String name,
String email
) {}
Controller
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest req) {
User user = userService.save(req);
RegisterResponse res = new RegisterResponse(user.getId(), user.getName(), user.getEmail());
return ResponseEntity.status(HttpStatus.CREATED).body(res);
}
7. Best Practices
- ✅ Keep record DTOs flat and purpose-specific.
- ✅ Use mapping libraries (e.g., MapStruct) for converting between entities and DTOs.
- ✅ Apply Bean Validation annotations to maintain security and consistency.
- ✅ Avoid logic in records — keep them data-only.
8. Conclusion
Using Java records with Spring Boot 3 brings a cleaner, more modern approach to defining API contracts. With records, your request/response bodies become immutable, concise, and type-safe, reducing boilerplate and improving code clarity. Just remember to validate inputs and shield sensitive data properly for secure APIs.




