Partner – Orkes – NPI EA (cat=Spring)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Partner – Orkes – NPI EA (tag=Microservices)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Partner – LambdaTest – NPI EA (cat=Testing)
announcement - icon

Browser testing is essential if you have a website or web applications that users interact with. Manual testing can be very helpful to an extent, but given the multiple browsers available, not to mention versions and operating system, testing everything manually becomes time-consuming and repetitive.

To help automate this process, Selenium is a popular choice for developers, as an open-source tool with a large and active community. What's more, we can further scale our automation testing by running on theLambdaTest cloud-based testing platform.

Read more through our step-by-step tutorial on how to set up Selenium tests with Java and run them on LambdaTest:

>> Automated Browser Testing With Selenium

Partner – Orkes – NPI EA (cat=Java)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

1. Overview

The Java standard for Bean Validation is available out of the box in Spring Boot via the Hibernate Validator reference implementation. It allows us to add standard annotations such as @NotNull to fields of request object classes to enable Spring to validate its inputs.

We can also extend the available validations. Additionally, we may need to use runtime data to implement our logic. For example, a value may only be valid if it can be found in our database or runtime configuration, making our validation algorithm stateful.

We may also require cross-field validation, where validation considers multiple values in an object to determine if related fields are valid.

In this tutorial, we’ll explore building custom validations.

2. Bean Validation Fundamentals

Validating a Java bean uses the JSR-380 framework. This provides general-purpose validation annotations in the jakarta.validation package, along with a Validator interface.

2.1. Annotations

To validate a field, we add an annotation to its declaration:

@Pattern(regexp = "A-\\d{8}-\\d")
private String productId;

During validation by the chosen instance of Validator, the annotation and its metadata (in this case, the pattern. regex) are used to determine the validation rules. The validator achieves this by finding a suitable validation implementation for each annotation when calling validate().

In this example, we’ve put a validation on a field, but we can also create type-level validations, as we’ll see later.

2.2. Validating Requests

In a Spring @RestController, we can annotate our request body with @Valid to ask for it to be validated automatically:

@PostMapping("/api/purchasing/")
public ResponseEntity<String> createPurchaseOrder(@Valid @RequestBody PurchaseOrderItem item) {
    // ... execute the order

    return ResponseEntity.accepted().build();
}

The body of our controller will only be called if the request is valid.

We can test this using a MockMvc test:

mockMvc.perform(post("/api/purchasing/")
    .content("{}")
    .contentType(MediaType.APPLICATION_JSON))
  .andExpect(status().isBadRequest());

Here, an empty JSON is not valid and results in an HTTP 400, BAD REQUEST.

3. Example Use Case

Next, let’s build a SaaS product that can process purchase orders via an API that receives PurchaseOrderItems:

public class PurchaseOrderItem {

    @NotNull
    @Pattern(regexp = "A-\\d{8}-\\d")
    private String productId;

    private String sourceWarehouse;
    private String destinationCountry;

    private String tenantChannel;

    private int numberOfIndividuals;
    private int numberOfPacks;
    private int itemsPerPack;

    @org.hibernate.validator.constraints.UUID
    private String clientUuid;

    // getters and setters
}

We’ve already added some built-in validations to this object. Initially, we require the productId to be non-null and match a specific pattern. We also expectĀ clientUuid to be a valid UUID. We’ve used both jakarta andĀ hibernate validations. But our requirements need additional rules, which require custom code and validators.

First, we want to make sure the productId matches a custom check digit algorithm.

Next, it’s not valid for an order to include packs and individual items, so onlyĀ numberOfIndividuals or numberOfPacks can be set.

Finally, the chosen warehouse must be able to ship to the destination country, and the tenantChannel must be configured on our server.

We’ll need a mixture of algorithmic and data-driven custom annotations to implement these validations. Our rules require some validation to involve multiple fields. Additionally, we’ll depend on data from both Spring properties and our database for validations, making these stateful validators.

4. Single Field Custom Validator

We can build a custom validator by providing both the annotation and the implementation of the algorithm. Let’s make a check digit validator for our product ID.

4.1. Define a Field Validation Annotation

The validation annotation needs some standard properties:

@Constraint(validatedBy = ProductCheckDigitValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ProductCheckDigit {
    String message() default "must have valid check digit";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

The annotation must have aĀ Retention ofĀ RUNTIME to be available to the validator, and we can decide whether the annotation targets fields or a whole type. In this case, we have a field-level annotation, indicated by the element type array containingĀ FIELD in the @Target annotation. Depending on the nature of the validation, we can even include function parameter validations.

The @Constraint annotation declares which class (or classes) handles this validation. This lets the validator run our custom validation code.

Finally, we should note that the default error message is in the message property here.

4.2. Creating the Custom Validator Class

Next, we need to create our validator class and override the isValid() method:

public class ProductCheckDigitValidator implements ConstraintValidator<ProductCheckDigit, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
       // implementation here
    }
}

Our check digit logic only needs to return false for the validator to be able to mark our field as invalid.

Specifically, this validator must implement the ConstraintValidator interface. We also declare the types this validator applies to. The first type declaration is our annotation, and the second type is the type to validate. In this case, our validator works on Strings annotated with ProductCheckDigit. To use our validation annotation on multiple types of fields, we’d write a custom validator class for each specific type, and have an array of validators in the validatedBy value of ourĀ @Constraint annotation.

4.3. Preparing Some Test Cases

Let’s set up our unit test and our request class before implementing the check digit logic.

First, we add the new annotation to our entity class:

public class PurchaseOrderItem {

    @ProductCheckDigit
    @NotNull
    @Pattern(regexp = "A-\\d{8}-\\d")
    private String productId;

    // ...
}

And then we create a unit test that has access to the validator:

@SpringBootTest
class PurchaseOrderItemValidationUnitTest {

    @Autowired
    private Validator validator;

    @Test
    void givenValidProductId_thenProductIdIsValid() {
        PurchaseOrderItem item = createValidPurchaseOrderItem();
        item.setProductId("A-12345678-6");

        Set<ConstraintViolation<PurchaseOrderItem>> violations = validator.validate(item);
        assertThat(violations).isEmpty();
    }
}

Here we have a test factory method to create a PurchaseOrderItem that’s completely valid, so we’re able to focus on the effect of individual fields in each of our tests. We’re also calling the validator directly to see what violations we find.

We should note that the Validator object can be provided by Spring to any of our components, so we’re not restricted to just using validation where Spring applies it automatically.

4.4. Implement the Check Digit

Our product identifier has two numeric sections – an eight-digit number and a check digit, which is the last digit of the sum of the first eight digits. Let’s extract these parts and test the check digit:

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) {
        return false;
    }

    String[] parts = value.split("-");

    return parts.length == 3 && checkDigitMatches(parts[1], parts[2]);
}

private static boolean checkDigitMatches(String productCode, String checkDigit) {
    int sumOfDigits = IntStream.range(0, productCode.length())
            .map(character -> Character.getNumericValue(productCode.charAt(character)))
            .sum();

    int checkDigitProvided = Character.getNumericValue(checkDigit.charAt(0));
    return checkDigitProvided == sumOfDigits % 10;
}

We validate the check digit by splitting the product ID and then summing the individual numbers of the middle numeric string.

4.5. When the Check Fails

Calling the validator provides a set of constraint violations. To make that easier to test with, let’s turn it into a list of field paths and error messages:

private static List<String> collectViolations(Set<ConstraintViolation<PurchaseOrderItem>> violations) {
    return violations.stream()
        .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
        .sorted()
        .collect(Collectors.toList());
}

Now we can check the error we get when a check digit doesn’t match:

PurchaseOrderItem item = createValidPurchaseOrderItem();
item.setProductId("A-12345678-1");

Set<ConstraintViolation<PurchaseOrderItem>> violations = validator.validate(item);

assertThat(collectViolations(violations))
  .containsExactly("productId: must have valid check digit");

Additionally, if we make the field null, we get multiple errors because our custom validator and the NotNull validator fail:

PurchaseOrderItem item = createValidPurchaseOrderItem();
item.setProductId(null);

Set<ConstraintViolation<PurchaseOrderItem>> violations = validator.validate(item);
assertThat(collectViolations(violations))
  .containsExactly(
    "productId: must have valid check digit",
    "productId: must not be null");

5. Multi-Field Validator

Now we’ve built a single field validator, let’s look at the rule where we must choose either individuals or packs of items.

5.1. Create the Validation Annotation

For multi-field validation, we need to apply the validation to the parent type:

@Constraint(validatedBy = ChoosePacksOrIndividualsValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ChoosePacksOrIndividuals {
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

This annotation has a target ofĀ TYPE, as it’s intended for use with theĀ PurchaseOrderItem type.

We need to validate the whole of ourĀ PurchaseOrderItem here, since a per-field validation would only look at a specific field with none of the surrounding context. Cross-field validation is achieved at the type level.

5.2. Create the Validator

The validator needs to create different constraint violations when both or neither of the quantities are set. It needs to avoid the default errors that the validation framework might create when isValid() returns false.

We start by creating the class, which binds the ability to validate aĀ PurchaseOrderItem with theĀ ChoosePacksOrIndividual annotation:

public class ChoosePacksOrIndividualsValidator 
  implements ConstraintValidator<ChoosePacksOrIndividuals, PurchaseOrderItem> {}

In the isValid() method, we start by disabling the default error messaging:

@Override
public boolean isValid(PurchaseOrderItem value, ConstraintValidatorContext context) {
    context.disableDefaultConstraintViolation();
    ...

This allows us to customize the error message instead of using the default message from the annotation.

Next, we can implement our logic for determining if the fields are valid, adding constraint violations to the fields that prove to be invalid:

boolean isValid = true;

if ((value.getNumberOfPacks() == 0) == (value.getNumberOfIndividuals() == 0)) {
    // either both are zero, or both are turned on
    isValid = false;
    context.disableDefaultConstraintViolation();
    if (value.getNumberOfPacks() == 0) {
        context.buildConstraintViolationWithTemplate("must choose a quantity when no packs")
                .addPropertyNode("numberOfIndividuals")
                .addConstraintViolation();
        context.buildConstraintViolationWithTemplate("must choose a quantity when no individuals")
                .addPropertyNode("numberOfPacks")
                .addConstraintViolation();
    } else {
        context.buildConstraintViolationWithTemplate("cannot be combined with number of packs")
                .addPropertyNode("numberOfIndividuals")
                .addConstraintViolation();
        context.buildConstraintViolationWithTemplate("cannot be combined with number of individuals")
                .addPropertyNode("numberOfPacks")
                .addConstraintViolation();
    }
}

if (value.getNumberOfPacks() > 0 && value.getItemsPerPack() == 0) {
    isValid = false;

    context.buildConstraintViolationWithTemplate("cannot be 0 when using packs")
            .addPropertyNode("itemsPerPack")
            .addConstraintViolation();
}

return isValid;

This algorithm checks whether we’ve got both fields at zero, or both fields at non-zero, which indicates that we’ve either specified both of them, or neither. It then adds a custom constraint violation for both fields to explain which mistake has been made.

5.3. Testing the Cross-Field Validation

First, we need to add the new annotation to our PurchaseOrderItem:

@ChoosePacksOrIndividuals
public class PurchaseOrderItem {
}

Then we can test an invalid combination:

PurchaseOrderItem item = createValidPurchaseOrderItem();
item.setNumberOfIndividuals(10);
item.setNumberOfPacks(20);
item.setItemsPerPack(0);

Set<ConstraintViolation<PurchaseOrderItem>> violations = validator.validate(item);
assertThat(collectViolations(violations))
  .containsExactly(
    "itemsPerPack: cannot be 0 when using packs",
    "numberOfIndividuals: cannot be combined with number of packs",
    "numberOfPacks: cannot be combined with number of individuals");

6. Stateful Validation Using Spring Properties

So far, we’ve bound static code to annotations and used algorithms with information known at compile time. We may need to use configuration properties to determine what’s valid.

6.1. Configuration of Valid Channels

Let’s say we had some runtime configuration properties:

com.baeldung.tenant.channels[0]=retail
com.baeldung.tenant.channels[1]=wholesale

This would be some data in ourĀ application.properties, which we load into a ConfigurationProperties class:

@ConfigurationProperties("com.baeldung.tenant")
public class TenantChannels {
    private String[] channels;

    // getter/setter
}

Now we want to be able to use this array of channels in a validator to check that the chosen channel in the request is available in this tenant.

6.2. Creating a Validator with a Bean Injected

Since Spring provides the validator, we can also inject other Spring beans into our validator. So we can autowire configuration properties into a custom validator:

public class AvailableChannelValidator implements ConstraintValidator<AvailableChannel, String> {

    @Autowired
    private TenantChannels tenantChannels;

}

Using the array from the properties object to check each channel at validation time is a little clunky. Let’s override theĀ initialize() method to turn that value into a set:

private Set<String> channels;

@Override
public void initialize(AvailableChannel constraintAnnotation) {
    channels = Arrays.stream(tenantChannels.getChannels()).collect(toSet());
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
    return channels.contains(value);
}

Now we have a validation annotation driven by the properties file of our server’s current Spring profile. We just need to annotate the field in our PurchaseOrderItem:

@AvailableChannel
private String tenantChannel;

7. Validation Based on Data

Once we can validate our fields with a Spring bean, we can use the same technique to leverage our database or other web services:

@Repository
public class WarehouseRouteRepository {

    public boolean isWarehouseRouteAvailable(String sourceWarehouse, String destinationCountry) {
        // read from database
    }
}

This repository can be injected into a validator:

public class AvailableWarehouseRouteValidator implements 
  ConstraintValidator<AvailableWarehouseRoute, PurchaseOrderItem> {
    @Autowired
    private WarehouseRouteRepository warehouseRouteRepository;

    @Override
    public boolean isValid(PurchaseOrderItem value, ConstraintValidatorContext context) {
        return warehouseRouteRepository.isWarehouseRouteAvailable(value.getSourceWarehouse(), 
          value.getDestinationCountry());
    }
}

Finally, since this validates multiple fields of the purchase order, we’ll add the related annotation at the class level:

@ChoosePacksOrIndividuals
@AvailableWarehouseRoute
public class PurchaseOrderItem {
    ...
}

8. Conclusion

In this article, we looked at how to add validations to fields and types. We wrote custom validation logic linked to custom annotations, and we saw how to operate on individual fields with default error messages or multiple fields with customized error messages.

Finally, we wrote stateful validators leveraging other Spring beans to make a validation based on runtime configuration or data.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

Partner – Orkes – NPI EA (cat = Spring)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

Partner – Orkes – NPI EA (tag = Microservices)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)