Sitemap
Aryan Chauhan

Aryan Chauhan

Software developer documenting my journey through Java, Spring Boot, backend engineering, DSA, and system design. Sharing practical lessons

Spring Boot Validation Groups Explained: Why Do We Need Empty Interfaces?

15 min read1 day ago

--

Understanding Jakarta Bean Validation, Marker Interfaces, and how to apply different validation rules for Create and Update REST APIs.

Press enter or click to view image in full size

Introduction

While building a Patient Management REST API using Java and Spring Boot, I came across something interesting.

I was creating a new interface called:

public interface CreatePatientValidationGroup {
}

At first, this looked a little strange.

An interface with no methods, no variables, and absolutely nothing inside it.

Naturally, a question came to mind:

Why would we create an interface if we aren’t going to put anything inside it?

After understanding the concept, I realized that this small interface solves a useful problem when building REST APIs: applying different validation rules depending on what operation we are performing.

This concept is called Validation Groups in Jakarta Bean Validation.

Before understanding Validation Groups, let’s understand what validation actually does in Spring Boot.

1. What Is Validation in Spring Boot?

Imagine we are building a Patient Management REST API.

Our API allows users to:

  • Register a new patient.
  • Update an existing patient’s information.
  • Retrieve patient details.
  • Delete a patient.

When someone sends a request to our API, we cannot blindly trust the incoming data.

For example, someone might send:

{
"name": "",
"email": "this-is-not-an-email",
"dateOfBirth": null
}

This request contains several problems:

  1. The patient’s name is empty.
  2. The email address is invalid.
  3. The date of birth is missing.

We should catch these problems before sending the data to our service layer or saving anything to the database.

This is where validation comes into the picture.

Validation is the process of checking whether incoming data satisfies the rules defined by our application.

Spring Boot supports Jakarta Bean Validation, which allows us to define validation rules using annotations directly on Java classes.

Some commonly used validation annotations are:

AnnotationPurpose@NotNullValue cannot be null.@NotEmptyValue cannot be null or empty.@NotBlankString cannot be null, empty or whitespace only.@EmailString must have a valid email format.@SizeChecks the minimum and maximum size.@MinNumber must be greater than or equal to a minimum.@MaxNumber must be less than or equal to a maximum.@PastDate must be in the past.@FutureDate must be in the future.@PatternString must match a specified regular expression.

Let’s understand how we use them.

1.1 Creating our first DTO with validation

In a REST API, we generally use DTOs (Data Transfer Objects) to receive data from the client.

For our patient service, let’s create a PatientRequestDTO.

import jakarta.validation.constraints.*;
import java.time.LocalDate;public class PatientRequestDTO {    @NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Please provide a valid email")
private String email;
@NotNull(message = "Date of birth is required")
@Past(message = "Date of birth must be in the past")
private LocalDate dateOfBirth;
// Getters and setters
}

Let’s understand each annotation.

@NotNull

@NotNull
private String name;

This ensures that the value is not null.

However, there is an important difference between @NotNull and @NotBlank.

Consider these values:

name = null;
name = "";
name = " ";
name = "Aryan";

@NotNull only rejects the first value.

It allows an empty string and a string containing spaces because both are non-null.

@NotBlank

@NotBlank
private String name;

This annotation is specifically useful for strings.

It rejects:

  • null
  • ""
  • " "

It accepts:

name = "Aryan";

This is generally more suitable for names, addresses and other mandatory text fields.

@Email

@Email
private String email;

This validates the email format.

For example:

aryan@gmail.com       Valid
aryan123@yahoo.com Valid
invalid-email Invalid

An important detail: @Email does not make a field mandatory. It considers null valid.

Therefore, when email is mandatory, we combine:

@NotBlank
@Email
private String email;

The first annotation checks whether a value was provided.

The second checks whether the provided value has a valid email format.

@Past

@Past
private LocalDate dateOfBirth;

This ensures that the supplied date is earlier than the current date.

For example:

2003-05-10    Valid
1998-10-15 Valid
2030-01-01 Invalid

Unlike @NotNull, @Past allows null values. That is why we combine it with @NotNull when the date is mandatory.

2. How Does Spring Actually Trigger Validation?

Writing validation annotations inside a DTO is not enough.

We also need to tell Spring to execute those validation rules when processing an incoming request.

Let’s look at our controller.

@PostMapping("/patients")
public ResponseEntity<PatientResponseDTO> createPatient(
@RequestBody
@Valid PatientRequestDTO patientDTO) {
    return ResponseEntity.ok(
patientService.savePatient(patientDTO)
);
}

There are three important things here.

@RequestBody

@RequestBody

This tells Spring that the incoming HTTP request body should be converted into a Java object.

For example, a client sends:

{
"name": "Aryan",
"email": "aryan@gmail.com",
"dateOfBirth": "2003-05-10"
}

Spring uses an HTTP message converter, typically Jackson, to convert this JSON into a PatientRequestDTO object.

@Valid

@Valid PatientRequestDTO patientDTO

This tells Spring to execute the Bean Validation constraints on the received object.

The flow looks like this:

  1. Client sends a JSON request.
  2. Spring converts JSON into a DTO.
  3. Spring executes the validation annotations.
  4. If validation succeeds, the controller method executes.
  5. The controller calls the service layer.
  6. The service processes the request.

But what happens when validation fails?

Spring raises a MethodArgumentNotValidException for the usual individual @RequestBody validation path.

This exception can be handled by a global exception handler, and the API can return HTTP 400 Bad Request.

We will implement this later.

For now, remember:

@Valid tells Spring to validate the object using the Default validation group.

And that brings us to the actual problem.

3. The Problem: Different Operations Can Have Different Validation Requirements

Let’s imagine our application has two operations.

Operation 1: Create a Patient

When registering a new patient, we want all the required information:

{
"name": "Aryan",
"email": "aryan@gmail.com",
"dateOfBirth": "2003-05-10"
}

All three fields are mandatory.

That makes sense because we are creating a completely new patient record.

Operation 2: Update a Patient

Now imagine we want to update an existing patient’s information.

Suppose our update operation allows the client to change the patient’s name while leaving the email and date of birth unchanged.

The request might look like:

{
"name": "Aryan Chauhan"
}

But remember our DTO:

public class PatientRequestDTO {
    @NotBlank
private String name;
@NotBlank
@Email
private String email;
@NotNull
private LocalDate dateOfBirth;
}

There is a problem here.

Spring will execute all the default validation constraints.

Even though we only want to update the name, validation will reject the request because email and dateOfBirth are missing.

Why?

Because our DTO says that all three fields are mandatory.

But our update operation does not require all three fields.

We now have a situation where:

  • Creating a patient requires all three fields.
  • Updating a patient may require only some of those fields.

The same DTO is being used for two different operations, but both operations don’t have the same validation requirements.

We could create two separate DTOs.

For example:

CreatePatientRequestDTO
UpdatePatientRequestDTO

That is a perfectly valid design.

However, what if we want to reuse the same DTO while applying different validation rules depending on the operation?

This is where Validation Groups become useful.

4. What Are Validation Groups?

Validation Groups allow us to organize constraints into different groups and execute only the group required for a particular operation.

In simple words:

Instead of validating every constraint every time, we can tell Spring which set of validation rules should execute.

For example:

OperationValidation rulesCreate PatientName, email and date of birth are mandatory.Update PatientName is mandatory; email and date of birth are optional.

We can create separate validation groups for these operations.

CreatePatientValidationGroup
UpdatePatientValidationGroup

But here comes the interesting part.

Both of these can be empty interfaces.

Why?

Because they are not responsible for implementing any logic.

They simply identify a collection of validation constraints.

5. What Is a Marker Interface?

Before continuing, let’s understand the Java concept behind these interfaces.

A marker interface is an interface that does not contain any methods or fields.

Its purpose is to mark or identify a class or a particular category of behavior.

For example, Java has a well-known marker interface:

import java.io.Serializable;
public class Patient implements Serializable {}

Serializable does not require us to implement any methods.

It identifies that the object supports Java serialization.

Similarly, in validation groups, an empty interface acts as an identifier for a particular set of validation constraints.

Let’s create our first interface.

CreatePatientValidationGroup.java

package com.pm.patientservice.dto.validators;
public interface CreatePatientValidationGroup {}

Now create another one.

UpdatePatientValidationGroup.java

package com.pm.patientservice.dto.validators;
public interface UpdatePatientValidationGroup {}

Notice that both interfaces are empty.

We do not need to write any methods inside them.

They simply provide a type that we can associate with our validation annotations.

Think of them as labels:

  • CreatePatientValidationGroup identifies validation rules for creating a patient.
  • UpdatePatientValidationGroup identifies validation rules for updating a patient.

Now let’s see how we actually use them.

6. How Do We Assign Validation Rules to Groups?

Remember our validation annotations?

For example:

@NotBlank
private String name;

Every Bean Validation constraint annotation has an attribute called groups.

Its purpose is to specify which validation group or groups the constraint belongs to.

For example:

@NotBlank(groups = CreatePatientValidationGroup.class)
private String name;

Let’s break this down.

What is groups?

groups =

This is an attribute provided by Bean Validation constraint annotations.

It accepts one or more group classes.

What is CreatePatientValidationGroup.class?

CreatePatientValidationGroup.class

This refers to the Class object representing our marker interface.

We are telling the validation provider:

“Execute this particular validation rule when the CreatePatientValidationGroup is selected.”

Notice that we use .class, not an object of the interface.

We are passing the type itself because the validation framework identifies groups using Java class types.

Can one constraint belong to multiple groups?

Absolutely.

For example:

@NotBlank(groups = {
CreatePatientValidationGroup.class,
UpdatePatientValidationGroup.class
})
private String name;

This means that the name must not be blank during both create and update operations.

A constraint can belong to more than one validation group.

That makes the system flexible.

7. Implementing Validation Groups in Our Patient DTO

Now let’s implement the complete example.

We want these rules:

During Create:

  • Name is mandatory.
  • Email is mandatory.
  • Email must have a valid format.
  • Date of birth is mandatory.
  • Date of birth must be in the past.

During Update:

  • Name is mandatory.
  • Email is optional, but if supplied, it must be valid.
  • Date of birth is optional, but if supplied, it must be in the past.

Let’s modify our DTO.

package com.pm.patientservice.dto;
import com.pm.patientservice.dto.validators.CreatePatientValidationGroup;
import com.pm.patientservice.dto.validators.UpdatePatientValidationGroup;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import java.time.LocalDate;public class PatientRequestDTO { @NotBlank(message = "Name is required")
private String name;
@NotBlank(
message = "Email is required",
groups = CreatePatientValidationGroup.class
)
@Email(
message = "Please provide a valid email",
groups = {
CreatePatientValidationGroup.class,
UpdatePatientValidationGroup.class
}
)
private String email;
@NotNull(
message = "Date of birth is required",
groups = CreatePatientValidationGroup.class
)
@Past(
message = "Date of birth must be in the past",
groups = {
CreatePatientValidationGroup.class,
UpdatePatientValidationGroup.class
}
)
private LocalDate dateOfBirth;
// Getters and setters
}

Let’s understand every field carefully.

Name field

@NotBlank(message = "Name is required")
private String name;

We haven’t specified any groups attribute.

This means the constraint belongs to the Default validation group.

We will see shortly how to include this constraint in our custom groups.

Email field

@NotBlank(groups = CreatePatientValidationGroup.class)

This means email cannot be blank during patient creation.

We haven’t assigned NotBlank to the update group.

Therefore, the update operation can omit the email.

Next:

@Email(groups = {
CreatePatientValidationGroup.class,
UpdatePatientValidationGroup.class
})

This checks the email format during both operations.

Remember that @Email accepts null.

Therefore:

  • During creation, @NotBlank makes email mandatory.
  • During updating, email may be omitted.
  • If an email is supplied, @Email checks its format.

Date of birth field

@NotNull(groups = CreatePatientValidationGroup.class)

This makes date of birth mandatory during creation.

Next:

@Past(groups = {
CreatePatientValidationGroup.class,
UpdatePatientValidationGroup.class
})

This checks the date during both operations.

Get Aryan Chauhan’s stories in your inbox

Join Medium for free to get updates from this writer.

Since @Past accepts null, the update operation can omit date of birth.

Now we have successfully assigned different validation rules to different operations.

But there is one important problem we still need to understand.

8. Understanding the Default Validation Group

This is one of the most important things to understand when working with Validation Groups.

Consider this annotation:

@NotBlank
private String name;

We did not specify a group.

So which group does this constraint belong to?

The answer is:

Default.class

Jakarta Bean Validation automatically assigns every constraint to the Default group when no group is explicitly specified.

You can also write it explicitly:

@NotBlank(groups = Default.class)
private String name;

Both are equivalent.

Now imagine we trigger validation like this:

@Validated(CreatePatientValidationGroup.class)

Which constraints will execute?

The constraints assigned to CreatePatientValidationGroup.

But what about constraints assigned to Default?

They will not automatically execute just because we selected a custom group.

That means our name validation could be skipped!

This is an important detail.

There are two common ways to solve this.

Approach 1: Assign common constraints to every required group

We can write:

@NotBlank(groups = {
CreatePatientValidationGroup.class,
UpdatePatientValidationGroup.class
})
private String name;

Now name validation explicitly belongs to both groups.

This approach works perfectly.

Approach 2: Make custom groups extend Default

We can also modify our interfaces.

CreatePatientValidationGroup.java

import jakarta.validation.groups.Default;
public interface CreatePatientValidationGroup extends Default {
}

UpdatePatientValidationGroup.java

import jakarta.validation.groups.Default;
public interface UpdatePatientValidationGroup extends Default {
}

What does this mean?

In Java, an interface can extend another interface.

Since both custom groups extend Default, validating either custom group also includes the constraints belonging to the Default group.

Conceptually:

CreatePatientValidationGroup
|
v
Default

And:

UpdatePatientValidationGroup
|
v
Default

Now, when we select either custom group, the Default constraints are included.

This allows us to keep common validation constraints simple:

@NotBlank
private String name;

We don’t need to explicitly assign the name constraint to both groups.

For our example, we can use Approach 2.

It keeps the DTO cleaner when common validation rules should apply to every operation.

9. How Do We Trigger a Custom Validation Group?

So far, we have:

  1. Created marker interfaces.
  2. Assigned validation constraints to groups.
  3. Understood how the Default group works.

But defining the groups is not enough.

We must tell Spring which group to execute when an API is called.

This is where @Validated comes in.

@Validated is a Spring annotation that supports specifying validation groups.

Its package is:

import org.springframework.validation.annotation.Validated;

Let’s modify our controller.

9.1 Create Patient API

@PostMapping("/patients")
public ResponseEntity<PatientResponseDTO> createPatient(
        @RequestBody
@Validated(CreatePatientValidationGroup.class)
PatientRequestDTO patientDTO
) {
return ResponseEntity.ok(
patientService.savePatient(patientDTO)
);
}

Look at this part:

@Validated(CreatePatientValidationGroup.class)

We are telling Spring to validate this request using the CreatePatientValidationGroup.

Therefore, Spring executes:

  • Constraints belonging to CreatePatientValidationGroup.
  • Constraints belonging to its parent groups, including Default, because our group extends Default.

It does not execute unrelated custom groups.

9.2 Update Patient API

Now let’s create the update endpoint.

@PatchMapping("/patients/{id}")
public ResponseEntity<PatientResponseDTO> updatePatient(
        @PathVariable UUID id,        @RequestBody
@Validated(UpdatePatientValidationGroup.class)
PatientRequestDTO patientDTO
) {
return ResponseEntity.ok(
patientService.updatePatient(id, patientDTO)
);
}

Notice the difference:

For creation:

@Validated(CreatePatientValidationGroup.class)

For updating:

@Validated(UpdatePatientValidationGroup.class)

The same DTO is used in both operations, but different validation groups are activated.

This is the main advantage of Validation Groups.

One DTO, multiple sets of validation rules.

An important API-design detail: our example requires the name even during PATCH. That is a deliberate rule in this example, not a requirement of PATCH itself. In an API where every field is optional during PATCH, use an appropriate update DTO or optional-field validation strategy instead.

10. How Does the Complete Request Flow Work?

Now let’s connect everything.

Imagine the client sends a request to:

POST /patients

The request body is:

{
"name": "Aryan",
"email": "aryan@gmail.com",
"dateOfBirth": "2003-05-10"
}

The flow is:

Step 1: Request received

Spring MVC receives the HTTP request.

Step 2: JSON converted into DTO

Jackson converts the JSON into a PatientRequestDTO object.

Step 3: Validation group selected

Spring sees:

@Validated(CreatePatientValidationGroup.class)

It selects the create validation group.

Step 4: Constraints executed

The validation provider checks:

Name       -> NotBlank
Email -> NotBlank + Email
Date -> NotNull + Past

Common constraints from Default are also included because our custom group extends Default.

Step 5: Validation result

There are two possible outcomes.

If validation succeeds, the controller method executes and calls the service.

If validation fails, Spring raises a validation exception and the service method is not executed.

For an invalid request, we can return HTTP 400 Bad Request.

The same flow happens for updates, except that the update validation group is selected.

11. Handling Validation Errors Using GlobalExceptionHandler

We should not leave validation errors without a proper response.

In our application, we can use @ControllerAdvice to handle exceptions globally.

Let’s understand the implementation.

package com.pm.patientservice.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> { errors.put(
error.getField(),
error.getDefaultMessage()
);
}); return ResponseEntity.badRequest().body(errors);
}
}

Let’s understand each part.

@ControllerAdvice

@ControllerAdvice

This tells Spring that the class can handle exceptions across multiple controllers.

Instead of writing exception-handling logic in every controller, we can centralize it in one place.

@ExceptionHandler

@ExceptionHandler(MethodArgumentNotValidException.class)

This tells Spring that this method handles MethodArgumentNotValidException.

This exception is raised when individual request-body validation fails in the normal Spring MVC validation path.

getBindingResult()

ex.getBindingResult()

This retrieves the validation result containing information about the errors.

getFieldErrors()

ex.getBindingResult().getFieldErrors()

This retrieves field-level validation errors.

For example, if the email is missing, the validation provider has information such as:

Field: email
Message: Email is required

getDefaultMessage()

error.getDefaultMessage()

This retrieves the custom message we specified in the validation annotation.

Finally:

return ResponseEntity.badRequest().body(errors);

We return HTTP 400 along with the validation errors.

An example response could be:

{
"name": "Name is required",
"email": "Email is required",
"dateOfBirth": "Date of birth is required"
}

Now clients can understand exactly what went wrong.

Note that a simple Map stores one message per field. For applications that need to return multiple messages for the same field, use a list of error objects instead.

12. Difference Between @Valid and @Validated

This is another common interview question.

Both annotations can trigger Bean Validation in Spring MVC, but they have different purposes.

@Valid@ValidatedPart of Jakarta ValidationProvided by SpringDoes not specify groups directlySupports specifying validation groupsUses the Default group when used normallyCan select a custom groupUseful for standard validationUseful for grouped validation

For example:

Normal validation:

@Valid
@RequestBody PatientRequestDTO patientDTO

Custom group validation:

@Validated(CreatePatientValidationGroup.class)
@RequestBody PatientRequestDTO patientDTO

Remember:

  • Use @Valid when you want ordinary Default group validation.
  • Use @Validated when you want to select a particular validation group.

They can also work together with nested DTO validation, but for our controller parameters, @Validated is sufficient to trigger the selected group.

13. Are Validation Groups Always Better Than Separate DTOs?

No.

This is an important design decision.

Validation Groups are useful, but they are not a replacement for every DTO design.

Consider these two approaches.

Approach A: Separate DTOs

CreatePatientRequestDTO
UpdatePatientRequestDTO

Each DTO has its own fields and validation annotations.

Advantages:

  • Clear separation between operations.
  • Each request structure is easy to understand.
  • Different operations can have completely different fields.

Disadvantages:

  • Some fields and validation rules may need to be repeated.
  • More DTO classes to maintain.

Approach B: Validation Groups

PatientRequestDTO

The same DTO is reused with different validation groups.

Advantages:

  • Less duplication when request structures are similar.
  • Validation rules are organized by operation.
  • Easier reuse of common constraints.

Disadvantages:

  • The DTO can become more complicated when many groups are introduced.
  • It can be harder to understand which constraints apply to a specific operation.

My takeaway:

Use Validation Groups when the same request structure is shared across operations but certain validation requirements differ.

Use separate DTOs when the request structures or business requirements are significantly different.

For example, a registration DTO and a password-reset DTO probably deserve separate request classes.

14. Common Mistakes When Using Validation Groups

Let’s go through a few mistakes that can cause confusion.

Mistake 1: Creating an interface but not assigning constraints to it

Creating this interface alone does not change anything:

public interface CreatePatientValidationGroup {
}

We must associate constraints with it:

@NotBlank(groups = CreatePatientValidationGroup.class)
private String name;

Otherwise, the interface has no custom validation rules associated with it.

Mistake 2: Forgetting the Default group

Consider:

@NotBlank
private String name;

And:

@Validated(CreatePatientValidationGroup.class)

If the custom group does not inherit from Default, the name constraint will not execute.

Either explicitly assign the constraint to the custom group or make the custom group extend Default.

Mistake 3: Thinking @Email makes a field mandatory

This is incorrect:

@Email
private String email;

It checks the email format, but it does not make the value mandatory.

Use:

@NotBlank
@Email
private String email;

when the email must be provided.

Mistake 4: Thinking groups automatically define execution order

Validation Groups do not automatically mean that one group executes before another.

They identify subsets of constraints.

If we need to control validation order, Jakarta Bean Validation provides @GroupSequence.

For example, we may want basic validation to pass before running more expensive validation checks.

Mistake 5: Thinking DTO validation replaces business validation

Consider checking whether an email already exists in the database.

This is not simply a format-validation problem.

Even if an email has a valid format, it may already belong to another patient.

We should enforce that business rule using the appropriate service or domain logic, usually with a database uniqueness constraint as an additional safeguard.

Bean Validation and business validation solve different problems.

15. When Should You Use Validation Groups?

Validation Groups are useful in situations such as:

User registration and profile updates

A password may be mandatory during registration but optional when updating a profile.

Create and update operations

A create request may require every field, whereas an update request may allow selected fields to be omitted.

Different workflow stages

An object may need to satisfy different sets of rules at different stages of its lifecycle.

Reusable request objects

When several operations share a DTO but have slightly different validation requirements, groups can prevent unnecessary duplication.

Conclusion

When I first encountered this concept, I genuinely wondered why we needed an empty interface.

But after understanding how Validation Groups work, the purpose became clear.

The interface is not there to implement any functionality.

It is simply a marker that identifies a particular set of validation constraints.

The important things to remember are:

  1. Validation annotations define the actual rules.
  2. Marker interfaces identify validation groups.
  3. The groups attribute associates constraints with those groups.
  4. @Validated tells Spring which group to execute.
  5. The Default group is used when no group is explicitly specified.
  6. Custom groups can extend Default to include common constraints.

The biggest lesson for me was this:

Validation is not only about checking whether data is valid. It is also about deciding which rules should apply at a particular point in the application’s workflow.

And that is exactly what Validation Groups help us achieve.

References and Further Reading

Thanks for reading!

--

--

Aryan Chauhan
Aryan Chauhan