Zum Hauptinhalt springen

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:

  1. What did 17 add that Week 1 already used?
  2. How do records show up in REST and configuration?
  3. Compact constructors and validation?
  4. When is a class still the right tool?
  5. 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 recordUse a class
HTTP DTO, command, value (Money)JPA @Entity
@ConfigurationProperties@Service / @Component
Test data, map keysTypes that must be proxied with a no-arg constructor and setters
sealed leaf with only dataTypes 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 / @JsonProperty on 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.

Next: Week 9 Day 3 — Sealed Types and Pattern Matching