Back to Blog
javarecordsspring-bootdtojava-21clean-code

Java Records as DTOs in Spring Boot: Replace Boilerplate with 1 Line (2026)

Java records eliminate the DTO boilerplate — no constructors, no getters, no equals/hashCode. But they have constraints that matter for Spring Boot integration. Here's everything you need.

J

JOptimize Team

May 30, 2026· 7 min read

A typical Spring Boot DTO before Java 14 looked like this: a class with private fields, a constructor, getters, equals(), hashCode(), and toString(). Add Lombok and you're at @Data, @AllArgsConstructor, @NoArgsConstructor, @Builder. It's boilerplate either way.

Java records collapse all of this into one line. They're immutable, they implement equals(), hashCode(), and toString() automatically, and they work with Jackson, Bean Validation, and Spring Data projections out of the box. Here's how to use them correctly.


Basic Record as DTO

// Before records — 30+ lines of boilerplate or Lombok annotations public class CreateOrderRequest { @NotNull private Long customerId; @NotEmpty private List<OrderItemRequest> items; private String notes; // Constructor, getters, equals, hashCode, toString... } // With records — 5 lines public record CreateOrderRequest( @NotNull(message = "Customer ID is required") Long customerId, @NotEmpty(message = "Order must have items") List<OrderItemRequest> items, @Size(max = 500) String notes ) {} // Access fields: req.customerId() // getter — no "get" prefix in records req.items() req.notes()

Records are final and immutable by default — all fields are set in the constructor, and there are no setters. This is a feature, not a limitation: DTOs shouldn't be mutable.


Jackson Integration

Jackson works with records out of the box in Spring Boot 2.3+ (Jackson 2.12+):

// Request deserialization — works automatically public record PlaceOrderRequest( Long customerId, List<OrderItemRequest> items, BigDecimal total ) {} @PostMapping("/orders") public ResponseEntity<OrderDto> createOrder( @RequestBody @Valid PlaceOrderRequest req) { // Jackson deserializes JSON into the record — no configuration needed return ResponseEntity.status(201).body(orderService.create(req)); } // Response serialization — works the same as a regular class public record OrderDto( Long id, OrderStatus status, BigDecimal total, LocalDateTime createdAt ) {}

For JSON with different field names:

public record UserDto( @JsonProperty("user_id") Long id, @JsonProperty("full_name") String name, @JsonIgnore String internalCode ) {}

Bean Validation on Records

public record RegisterUserRequest( @NotBlank(message = "Email is required") @Email(message = "Invalid email format") String email, @NotBlank @Size(min = 8, message = "Password must be at least 8 characters") @Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).+", message = "Password must contain uppercase and digit") String password, @NotBlank String firstName, @NotBlank String lastName, @Past(message = "Birth date must be in the past") LocalDate birthDate ) {} // Controller — @Valid triggers validation @PostMapping("/users") public ResponseEntity<UserDto> register(@RequestBody @Valid RegisterUserRequest req) { return ResponseEntity.status(201).body(userService.register(req)); } // Invalid request → 400 with validation errors automatically

Records in JPA Projections

Records work perfectly as JPA projection types:

// Record projection — more explicit than interface projections public record OrderSummary( Long id, OrderStatus status, String customerName, BigDecimal total ) {} // Spring Data will auto-detect and use the constructor @Query(""" SELECT new com.example.dto.OrderSummary( o.id, o.status, c.name, o.total ) FROM Order o JOIN o.customer c WHERE o.customerId = :customerId """) List<OrderSummary> findSummariesByCustomerId(@Param("customerId") Long customerId); // Or use interface projection if you prefer: public interface OrderSummaryProjection { Long getId(); OrderStatus getStatus(); // Records are often cleaner for explicit DTO projections }

Compact Constructor for Validation and Transformation

Records support a compact constructor for adding validation logic:

public record Money(BigDecimal amount, String currency) { // Compact constructor — no parameter list, accesses fields directly public Money { Objects.requireNonNull(amount, "Amount cannot be null"); Objects.requireNonNull(currency, "Currency cannot be null"); if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Amount cannot be negative: " + amount); } // Normalize currency = currency.toUpperCase().strip(); // Modifies the field before assignment amount = amount.setScale(2, RoundingMode.HALF_UP); } } // Records are great for value objects in domain-driven design: public record CustomerId(Long value) { public CustomerId { if (value == null || value <= 0) throw new IllegalArgumentException(); } } public record Email(String value) { public Email { if (value == null || !value.contains("@")) throw new IllegalArgumentException("Invalid email: " + value); value = value.toLowerCase(); } }

Nested Records for Complex DTOs

public record PlaceOrderRequest( @NotNull Long customerId, @NotNull ShippingAddress shippingAddress, @NotEmpty List<OrderLineItem> items ) { public record ShippingAddress( @NotBlank String street, @NotBlank String city, @NotBlank String postalCode, @NotBlank String country ) {} public record OrderLineItem( @NotNull Long productId, @Min(1) int quantity, @NotNull BigDecimal unitPrice ) {} } // Usage: PlaceOrderRequest.ShippingAddress, PlaceOrderRequest.OrderLineItem

When NOT to Use Records

Records have constraints that make them unsuitable for some use cases:

JPA entities: Records are final and immutable — Hibernate requires mutable classes with no-arg constructors for its proxy mechanism. Never use records for @Entity classes.

Classes that need inheritance: Records can't extend other classes (they implicitly extend java.lang.Record). Use regular classes or interfaces for hierarchies.

Mutable state: Records are immutable by design. If you need setStatus() or similar mutating methods, use a regular class.

// WRONG — records can't be JPA entities @Entity public record Order(Long id, OrderStatus status) {} // Compile error // CORRECT — use classes for entities @Entity public class Order { @Id private Long id; private OrderStatus status; // Regular mutable entity } // Records are perfect for: DTOs, value objects, projections, immutable config public record AppConfig(String baseUrl, int timeout, boolean retryEnabled) {}

Summary

Java records eliminate DTO boilerplate — one line replaces 30+ lines of class definition. They work with Jackson serialization, Bean Validation, JPA projections, and work as value objects with compact constructors for validation. Don't use them for JPA entities (not compatible with Hibernate's proxy mechanism) or for classes that need inheritance or mutable state. For everything else, they're the right default.


JOptimize: Detect Records Used as JPA Entities

JOptimize flags records used where Hibernate entities are expected — one of the most common subtle bugs when adopting records.

Write modern Java. Avoid the traps.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.