Zum Hauptinhalt springen

Week 1 Day 5 — Records as Value Objects

Goal

Today I want records as the modern way to model values, and a clear line against entities and Spring beans.

Main questions:

  1. What is a record?
  2. What does the compiler generate?
  3. What is a value object?
  4. When is a record the wrong tool?
  5. How do records show up in Spring Boot APIs?

1. What a record is

A record is a special class for transparent, shallowly immutable data.


public record Money(String currency, long cents) {}

The compiler generates:

  • a final class
  • private final fields for each component
  • a canonical constructor
  • accessors currency() and cents() (not getCurrency())
  • equals and hashCode on all components
  • a readable toString

I can add compact constructors, extra methods, and static factories. I cannot add extra instance fields. I cannot extend another class (records already extend java.lang.Record). I can implement interfaces.

Memory sentence:

A record is a value: equality by data, fields final, no hidden extra state.


2. Compact constructors and validation


public record Money(String currency, long cents) {
public Money {
Objects.requireNonNull(currency, "currency");
if (cents < 0) {
throw new IllegalArgumentException("cents");
}
currency = currency.toUpperCase(Locale.ROOT);
}
}

The compact constructor runs before the fields are assigned. Assigning to the parameters is how I normalize. I still get the generated accessors and equals.

Copy mutable components here:


public record Team(List<String> members) {
public Team {
members = List.copyOf(members);
}
}

Without that copy, equals/hashCode sit on a list the caller can still change — a broken key.


3. Value object vs entity vs bean

KindIdentityMutationTypical Java
ValueNone. Two with the same data are interchangeable.Prefer none.record, final class
EntityHas an id that survives field changes.Mutable. JPA loads and writes fields.@Entity class
Service beanIdentity is “the collaborator in the context.”Methods have effects; fields should be collaborators only.@Service class

Money a = new Money("EUR", 100);
Money b = new Money("EUR", 100);
a.equals(b); // true — values

User ada = new User(1L, "Ada");
User also = new User(1L, "Ada Lovelace");
// entity equality is a later JPA design choice; it is not automatic

Do not make JPA entities records. Hibernate needs a no-arg constructor, non-final fields, and proxies. Records fight all three.

Do not make @Service a record. A service is behavior with injected collaborators, not a value.


4. Records vs JavaBeans vs Lombok

JavaBean style: no-arg constructor, getters, setters. Jackson and some tools like it. It is a poor domain model because every field is mutable and identity is unclear.

Lombok @Value / @Builder can approximate records on older Java. On JDK 17+ I prefer records for DTOs and value types unless the team standard is Lombok.

Accessors: Jackson on Spring Boot 3 understands record accessors. Request/response DTOs as records are the default I want.


public record CreateOrderRequest(String sku, int quantity) {}
public record OrderResponse(long id, String status) {}

Keep validation annotations on the record components (@NotBlank, @Min) and let Spring MVC run them. That is Week 4 of the Spring book. The Java idea is: the DTO is a value, not a mutable bag.


5. Spring connection

Good uses:

  • API request/response bodies
  • command / query objects passed into services
  • configuration as @ConfigurationProperties records (supported in recent Boot)
  • map keys and event payloads

Bad uses:

  • JPA @Entity
  • Spring @Service / @Component
  • anything that must be subclassed by a library proxy that needs a no-arg constructor and setters

Memory sentence:

Records for values and DTOs. Classes for entities and services.


6. Common traps

Trap 1: Record with a mutable list and no copy.
Shallow immutability. equals can change after the object is used as a key.

Trap 2: Using records as JPA entities because they look short.
Proxies, dirty checking, and no-arg constructors need a normal class.

Trap 3: Expecting getCurrency().
The accessor is currency(). Jackson is fine. Some old Bean-based libraries are not.

Trap 4: A 20-component record “DTO” that is really a mutable form.
If I need 20 optional setters, I do not have a value object. I have an unmodeled form.


Practice Questions and Answers

Question 1

What does the compiler generate for a record?

Answer:

A final class with private final fields, a canonical constructor, accessors named after the components, and equals, hashCode, and toString based on those components.


Question 2

Why copy a List component in the compact constructor?

Answer:

The record field cannot be reassigned, but the list can still be mutated through the caller’s reference. List.copyOf stores an unmodifiable snapshot so the value cannot change under equals and hashCode.


Question 3

Should a JPA entity be a record?

Answer:

No. JPA providers need a no-arg constructor, the ability to set fields after construction, and often a proxy subclass. Records are final, shallowly immutable, and have no no-arg constructor unless every component has a default — which still fights the proxy model.


Question 4

When do I choose a record over a Lombok @Value class?

Answer:

On JDK 17+, records are language-level, IDE- and Jackson-friendly, and need no annotation processor. I use them for DTOs and small values. I keep a class when I need inheritance, extra fields, or JPA.


Question 5

Is a Spring singleton a value object?

Answer:

No. It has identity in the container (the bean), it is shared, and its job is behavior. I inject it. I do not compare two services with equals. Values are Money and CreateOrderRequest. Services are OrderService.


Memory sentences

A record is a value: equality by data, fields final, no extra instance state.

Copy mutable record components or accept a leak.

Records for DTOs and values. Classes for entities and Spring beans.

Next: Week 1 Review