Skip to main content

How and why I designed a business validatoion framework

Greetings!

Business validations are a crucial part of any application. We faced use cases with too many validations. It is hard to maintain and extend the solutions with such a number of validations.

This is when I thought to try a solution even though there was no such task in the backlog. Hence I used my own personal time for this.

Features

  • Execute a larger number of validations in single use case
  • Reusability by sharing the same validation in different use cases
  • Skip non-mandatory validations by user choice (yes/no dialogs)
  • Expose validations via REST API
  • Extendability by adding/removing validations
  • No performance impact
  • Easy to use in future developments

It all started with an Interface

My quick solution was to introduce a common interface by utilizing Java generics and Java 8 new features. I did not have all the features in mind when introducing this. However, that is the power of having a proper interface.
public interface Validator<T> {

  void validate(T object) throws ValidationException;

  String getId();

  default boolean isMandatory() {
    return true;
  }

}
One improvement here is to return the result object instead of throwing an exception. But anyway, this works perfectly for us.

Skip non-mandatory validations

Usually, UIs have yes/no dialogs so that the end user can decide whether to proceed or cancel. As this kind of validation can happen deliberately or by mistake. The challenge here is to make it server logic.

Because of this reason I have "isMandatory" default method in my interface that returns true and an "id" to track it.

Expose via REST API

This was the trickiest part. We develop our REST APIs as business endpoints. Hence we need to use proper REST conventions. This has 2 parts.

One is to inform the user about validation failures. That is why I have a code. We return 400 status with an error code and message. By using the code, the consumer can decide how to present the failure to the end user. Also, i18n can be decided on the consumer end by using the code.

Another thing is how can the consumer do yes/no operations. We introduced "userConfirmation" resource with business names and map it to the relevant validator in the backend.

Executing multiple validations

This is the first problem I wanted to solve. You can't call validations one by one (of cause you can but that is not handy). My choice was to use a chain where I formed validations in a chain (an ArrayList) and loop them all at once.
public final class ValidatorExecutor<T> {
  private final List<Validator<T>> validators;

  private ValidatorExecutor(final List<Validator<T>> validators) {
    this.validators = validators;
  }

  public void execute(T object) throws ValidationException {
    for (Validator<T> validator : validators) {
      validator.validate(object);
    }
  }
  
  public static class ValidatorExecutorBuilder<T> {
    private final List<Validator<T>> validators;
    private List<String> skippableIds;

    public ValidatorExecutorBuilder(final List<Validator<T>> validators) {
      this.validators = validators;
      this.skippableIds = new ArrayList<>();
    }

    public ValidatorExecutorBuilder<T> skip(List<String> skippableIds) {
      this.skippableIds = skippableIds;
      return this;
    }

    public ValidatorExecutor<T> build() {
      List<Validator<T>> mandatoryValidators = // logic;
      List<Validator<T>> optionalValidators = // logic;

      List<Validator<T>> executableValidators = new ArrayList<>();
      executableValidators.addAll(mandatoryValidators);
      executableValidators.addAll(optionalValidators);

      return new ValidatorExecutor<>(executableValidators);
    }
  }
}
In Factory;
return new ValidatorExecutor.ValidatorExecutorBuilder<>(validators);
Before the logic;
List<> skipIds = userConfirmations.convertToIds();
ValidatorExecutor<> validatorExecutor = factory.getExecutroBuilder().skip(skipIds).build();
validatorExecutor.execute(params);
I might have added the full executor creation into the factory but this is ok

Re-use validations

The same validation can be applied to multiple use cases. How can I reuse validations for separate models? This is why validators accept only required (specific to the validator) parameters. Then for specific use cases, I decorate them by creating intermediate decorators that convert the validator chain's parameter to the validator's parameter.
public abstract class ExtendedValidator<T, R> implements Validator<T> {
  protected final Validator<R> validator;

  protected ExtendedValidator(final Validator<R> validator) {
    this.validator = validator;
  }

  public String getId() {
    return validator.getId();
  }

  public boolean isMandatory() {
    return validator.isMandatory();
  }
}

Handling data fetching

Well, having a large number of validations that loads many data can have a performance impact. The solution here is to load everything in the beginning before executing the validator chain. That means validators will not fetch data. It is the initial controller's job to fetch data necessary for validations.


Conclusion

I have summarized how and why I created our business validator framework. Even though I have not added full classes/diagrams this has all the main ideas for anyone interested.

Happy learning guys ☺

Comments