Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · senior level

How do binding and validation work — @Valid, groups, custom validators — and where should validation live?

🎤 Say this first

Binding converts raw input to your types (converters/Jackson); Bean Validation (Jakarta, Hibernate Validator) then enforces declarative constraints when the parameter is annotated @Valid/@Validated — failures become MethodArgumentNotValidException → 400/422 via your advice. Layer the concern: syntactic validation (shape, ranges, formats) on request DTOs at the edge; semantic/business rules (uniqueness, balance sufficiency, state transitions) in the domain/service layer as explicit code — not annotations. @Validated on a service class enables method-level constraint enforcement (via AOP, with its proxy caveats).

The full picture

Edge validation with records + a custom constraint
public record CreateAccountRequest(
    @NotBlank @Email String email,
    @NotNull @Past LocalDate birthDate,
    @Valid @NotNull AddressDto address,          // cascades into nested object
    @StrongPassword String password              // custom constraint
) {}

@PostMapping("/accounts")
AccountDto create(@Valid @RequestBody CreateAccountRequest req) { … }

// Custom constraint = annotation + validator:
@Constraint(validatedBy = StrongPasswordValidator.class)
@Target(FIELD) @Retention(RUNTIME)
public @interface StrongPassword {
    String message() default "password too weak";
    Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};
}
class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
    public boolean isValid(String v, ConstraintValidatorContext c) {
        return v != null && v.length() >= 12 && hasMixedClasses(v);
    }
}
  • @Valid (Jakarta) triggers validation & cascades; @Validated (Spring) adds groups and enables method validation on non-controller beans.
  • Validation groups (e.g., OnCreate, OnUpdate) solve 'id required on update, forbidden on create' — though separate request records per operation is often the cleaner answer.
  • Cross-field rules: class-level constraints (@PasswordsMatch on the record) or explicit code; don't contort field-level annotations.
  • Business rules as annotations (@UniqueEmail hitting the DB inside a validator) are a smell: hidden I/O, no transaction context, surprises under retry. Keep I/O-dependent rules in the service where they're testable and transactional.

🔄 Likely follow-up questions

  • How do binding errors differ from validation errors (400 vs 422 debate again)?
  • How does method validation on services actually work (proxy!) and when does it silently not run?
  • How do you localize validation messages?