Using Value Objects (VOs) for validation is a great approach to encapsulate constraints and avoid repeated annotations. However, as you pointed out, a fundamental property of value objects is that they should not be null, which conflicts with the optional fields in your BankUpdateRequest
.
Here are some ways to balance clean design with flexibility for updates:
null
in DTOsInstead of making fields @NotNull
in DTOs, allow null
for update DTOs.
The validation logic moves to the VO constructors.
public record Name(String value) { public Name { if (value != null) { if (value.isBlank()) throw new IllegalArgumentException("The name is a required field."); if (value.length() > 255) throw new IllegalArgumentException("The name cannot be longer than 255 characters."); if (!value.matches("^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$")) throw new IllegalArgumentException("The name can only contain alphabetic characters."); } } }
Then your DTOs become:
public record BankCreateRequest(@NotNull Name name, @NotNull BankAccountNumber bankAccountNumber, Fax fax) {} public record BankUpdateRequest(Name name, BankAccountNumber bankAccountNumber, Fax fax) {}
This way, BankCreateRequest
enforces values.
BankUpdateRequest
allows null
, and validation happens only if values are present.
Pros:
Removes repetitive bean validation.
Encapsulates validation logic in VOs.
Works well with both create and update.
Cons:
You must manually handle null checks in constructors.
Validation happens at object creation rather than at request validation (which might be less intuitive for some teams).
An alternative is keeping Spring’s validation for DTOs but still using Value Objects:
public record BankCreateRequest( @NotNull Name name, @NotNull BankAccountNumber bankAccountNumber, Fax fax ) {} public record BankUpdateRequest( Name name, BankAccountNumber bankAccountNumber, Fax fax ) {}
Then keep Hibernate Validator annotations inside the VOs:
@Value public class Name { @NotBlank(message = "The name is a required field.") @Size(max = 255, message = "The name cannot be longer than 255 characters.") @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.") String value; }
Key Trick: Use @Valid in DTOs to trigger Spring validation:
public record BankCreateRequest(@Valid @NotNull Name name, @Valid @NotNull BankAccountNumber bankAccountNumber, @Valid Fax fax) {} public record BankUpdateRequest(@Valid Name name, @Valid BankAccountNumber bankAccountNumber, @Valid Fax fax) {}
Pros:
Fully integrates with Spring’s validation.
No need for manual exceptions in VOs.
Works well with null updates.
Cons:
Spring validation is still needed in VOs.
Slightly more boilerplate.
Approach 1 (manual validation in VO constructor) is better if you want pure DDD-style Value Objects but requires handling null checks yourself.
Approach 2 (Spring Validation inside VOs) is simpler and integrates better with Spring Boot, so it’s often the more practical choice.
➡ Recommendation: Use Approach 2 (@Valid
+ VOs). It’s simpler, aligns with Spring Boot, and still removes duplication.