Week 9 Day 2 — Records in APIs
Goal
Today I want records as the default DTO and value type on Java 17+, with the same limits as Week 1 Day 5, now in a Boot API.
Main questions:
- What did 17 add that Week 1 already used?
- How do records show up in REST and configuration?
- Compact constructors and validation?
- When is a class still the right tool?
- How does Jackson treat accessors?
1. Recap in one picture
public record CreateOrderRequest(String sku, int quantity) {
public CreateOrderRequest {
Objects.requireNonNull(sku, "sku");
if (quantity <= 0) {
throw new IllegalArgumentException("quantity");
}
sku = sku.strip();
}
}
The compiler still generates final fields, canonical constructor, accessors sku() / quantity(), equals / hashCode / toString. Compact constructor validates and normalizes parameters before the fields are set.
Memory sentence:
Records for values and DTOs. Classes for entities and Spring beans.
2. REST bodies
@PostMapping("/orders")
OrderResponse place(@Valid @RequestBody CreateOrderRequest request) {
return OrderResponse.from(orders.place(request));
}
public record OrderResponse(long id, String status) {
static OrderResponse from(Order order) {
return new OrderResponse(order.id(), order.status().name());
}
}
Bean Validation annotations go on components: @NotBlank String sku, @Min(1) int quantity. Spring MVC binds JSON to the record. Accessors are sku(), not getSku(). Jackson on Boot 3 understands that.
I do not return JPA entities. I map to a response record.
3. Configuration
Recent Boot can bind @ConfigurationProperties to a record:
@ConfigurationProperties(prefix = "app.orders")
public record OrdersProperties(int maxLines, Duration timeout) {}
Immutable, constructor-bound, easy to test. Enable with @EnableConfigurationProperties or @ConfigurationPropertiesScan. Missing properties fail at startup instead of leaving zeros in a mutable bean — that is the point.
4. Copying mutable components
public record Team(List<String> members) {
public Team {
members = List.copyOf(members);
}
}
Without the copy, equals/hashCode sit on a list the caller can still change (Week 1 and Week 6 keys). List.copyOf rejects nulls and stores an unmodifiable snapshot.
Nested records are fine: record Line(String sku, int qty) {} inside CreateOrderRequest.
5. When I still write a class
| Use a record | Use a class |
|---|---|
HTTP DTO, command, value (Money) | JPA @Entity |
@ConfigurationProperties | @Service / @Component |
| Test data, map keys | Types that must be proxied with a no-arg constructor and setters |
sealed leaf with only data | Types with identity and lifecycle |
Hibernate wants a no-arg constructor, non-final fields, often a proxy subclass. Records fight all three. I do not “simplify” an entity into a record.
Lombok @Value on 17+ is optional; the language already has records. I follow the team. I can explain both.
6. Spring connection
@JsonIgnore/@JsonPropertyon record components work with Jackson 2.12+.- OpenAPI / springdoc reads record accessors.
ProblemDetail(Week 4) is a class from the framework; my error payloads can still be records if I build them myself.
7. Common traps
Trap 1: Record as @Entity.
Trap 2: Mutable list component with no copy.
Trap 3: Expecting getSku() from libraries that only understand JavaBeans.
Trap 4: A 25-component “request” record that is a form, not a value. Split commands.
Trap 5: Using a record as a @Service so it looks modern.
Practice Questions and Answers
Question 1
Records vs JavaBeans vs Lombok for a REST DTO?
Answer:
On 17+ I use a record: immutable components, generated equals/hashCode, Jackson-friendly accessors. JavaBeans (getters/setters) are a mutable bag. Lombok @Value approximates records on older Java; I do not need it for new DTOs on 17+.
Question 2
Why not a JPA entity as a record?
Answer:
JPA providers need a no-arg constructor, settable fields, and often a proxy subclass. Records are final, shallowly immutable, and have no no-arg constructor. Entities have identity and a lifecycle; records are values.
Question 3
Where do I validate a request record?
Answer:
Compact constructor for invariants I always want (quantity > 0). Bean Validation annotations for HTTP 400 via Spring MVC. Both can coexist. The service may still throw domain exceptions for rules that need the database.
Question 4
How does Jackson find record properties?
Answer:
It uses the canonical constructor to deserialize and the accessor methods (sku(), not getSku()) to serialize. Boot 3’s Jackson is set up for that. I do not add dummy setters.
Question 5
When is a compact constructor required?
Answer:
When I must reject or normalize input: nulls, ranges, strip(), List.copyOf. If the data is already trusted (a response I built), an empty record body is enough.
Memory sentences
Records for values and DTOs. Classes for entities and Spring beans.
Compact constructors validate parameters. Copy mutable components.
Jackson on Boot 3 binds records through the canonical constructor.