How I fixed my mess of custom validations

Kavindu Perera
3 min readJul 8, 2023

What I initially created was a single class for validation for similar business scenarios.

Something like this,

public interface Validator {
void validate();
}

public class ValidatorImpl implements Validator {

@Override
public void validate () {
// call some private mothods to check and collection validation failure details
// throws a ValidationException
}

}

When business scenarios grew, I found myself adding methods with boolean returns to skip already confirmed mandatory value validation for a business scenario from throwing validation exceptions for another business scenario where the said value was optional and most business scenarios look for a set of common values to be mandatory.

I had to think of a clear solution considering both growing business scenarios and ever-changing mandatory/optional values.

The solution: The Validation Triad

As the name suggests, there are three components.

public interface ValidationService {
void validate();
}

public interface Validator {
void runValidator();
Map<String, String> getFailures();
}

public interface ValidationProvider {
void validateSomeField(Map<String, String> allFailures);
void validateSomeOtherField(Map<String, String> allFailures);
//...
}

How this works,

Implementation of ValidationProvider contains validations for all the values of all business scenarios. It does not matter whether business scenario “A” needs to validate “someField” or “B” needs to validate “someOtherField”. The implementation of ValidationProvider only carries instructions for what needs to be done to validate the said value. If validation fails, the allFailures map is updated with the failure field name and failure message.

The implementation of Validator is done for each business scenario. The ValidationProvider is a dependency and
Example,

public class ScenarioValidator implements Validator {
private final ValidationProvider validationProvider;

public SenatioAValidator(ValidationProvider validationProvider) {
this.validationProvider = validationProvider;
}

Map<String, String> allFailures = new HashMap<>();

@Override
public void runValidator() {

validationProvider.validateSomeField(allFailures);
//..

}

@Override
public Map<String, String> getAllFailures() {
//..
}
}

As in the above example, a validator class is created for each business scenario.

The implementation of ValidationService contains dependencies of available scenario validators and a mechanism to identify the correct scenario validator each time the business scenario is called for.
Also, a mechanism to call Validator.runValidator() the parent method of scenario validators and throw a validation exception if the failure map is not empty.

public class ValidationServiceImpl implements ValidationService {

private final ScenarioValidator scenarioValidator;
//.. other scenario validators goes here

public ValidationServiceImpl(ScenarioValidator scenarioValidator) {

this.scenarioValidator = scenarioValidator;
//..

}

@Override
public void validate() {

Validator validator = getValidator();
if (Objects.isNull(validator))
throw new NullPointerException();

validator.runValidator(dtvCLMOrder, appRequestData);

Map<String, String> allFailures = validator.getFailures();
if (!allFailures.isEmpty()) {
throw new ValidationException();
}

}

//mechanism to get validator instance
private Validator getValidator() {

//..

}

}

Add the ValidatorService as dependency where you need to trigger the validation stage and call ValidatorService.validate()

Wrap up,

This is only the high-level approach to validating an ever-changing list of values.
Even though the actual implementation is not described here, this is the skeleton which the actual implementation is built upon.
This solution was inspired by S.O.L.I.D principles.

Thank you for reading this far…!!!

--

--