package com.example.signup.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "{signup.email.alreadyTaken}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.signup.validation;
import com.example.signup.user.UserRepository;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.stereotype.Component;
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
private final UserRepository userRepository;
public UniqueEmailValidator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null) {
return true; // leave null handling to @NotBlank
}
String normalized = email.trim().toLowerCase();
return !userRepository.existsByEmailIgnoreCase(normalized);
}
}
package com.example.signup.auth;
import com.example.signup.validation.UniqueEmail;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class SignupRequest {
@NotBlank
@Size(max = 80)
private String displayName;
@NotBlank
@Email
@UniqueEmail
private String email;
@NotBlank
@Size(min = 8, max = 100)
private String password;
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.example.signup.auth;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final SignupService signupService;
public AuthController(SignupService signupService) {
this.signupService = signupService;
}
@PostMapping("/signup")
public ResponseEntity<SignupResponse> signup(@Valid @RequestBody SignupRequest request) {
SignupResponse response = signupService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
This snippet demonstrates how to enforce database-backed uniqueness during a signup flow using JSR-380 Bean Validation rather than scattering ad-hoc checks through service code. The core idea is that uniqueness is a validity concern of the request payload, so it can be expressed declaratively with a custom constraint annotation and a validator that queries the database.
In UniqueEmail annotation, the marker is defined with @Constraint(validatedBy = UniqueEmailValidator.class), which tells Hibernate Validator which class implements the rule. The mandatory message, groups, and payload members follow the Bean Validation contract; message uses a template key so it can be externalized into ValidationMessages.properties. Declaring @Target({FIELD, PARAMETER}) and @Retention(RUNTIME) keeps the annotation usable on DTO fields and readable via reflection at runtime.
UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String>. Because it is a Spring @Component, the container injects UserRepository through constructor injection — a capability plain JSR-380 lacks, which is precisely why Spring's LocalValidatorFactoryBean wires validators as beans. The isValid method deliberately returns true for null, delegating null-checking to @NotBlank so responsibilities stay single-purpose. It normalizes the address to lowercase and trims it before calling existsByEmailIgnoreCase, avoiding case-variant duplicates. This means the validator performs a real query, so it should be paired with a database unique index to close the race window between validation and insert.
SignupRequest DTO composes the constraints: @NotBlank, @Email, and the custom @UniqueEmail sit together on the email field, layering format and uniqueness rules. AuthController triggers the whole chain simply by annotating the parameter with @Valid; when any constraint fails, Spring throws MethodArgumentNotValidException before the method body runs.
The trade-off worth noting is cost and timing: each validation issues a query, and validation is not transactional, so two concurrent signups can both pass. The pattern's value is clean, declarative, reusable rules with friendly field-level error messages, while the unique index remains the ultimate guarantee of correctness.
Related snips
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.