J.Cowlory
J.Cowlory

Reputation: 61

Validation in service layer (SpringBoot)

I have a DTO which is validated at Controller layer with a mix of BeanValidation (javax.validation) and a custom Validator (org.springframework.validation.Validator). This way I can check if the input provided is valid and then convert the DTO in an entity and forward it to the Service layer.

    @Data
    public class UserDTO {

            @NotBlank
            @Size(max = 25)
            private String name;

            @NotNull
            private Date birthday;

            @NotNull
            private Date startDate;

            private Date endDate;

            private Long count;

    }

    public class UserDTOValidator implements Validator {
        private static final String START_DATE= "startDate";
        private static final String END_DATE= "endDate";
        private static final String COUNT= "count";

        @Override
        public boolean supports(Class<?> clazz) {
            return UserDTO.class.isAssignableFrom(clazz);
        }
        @Override
        public void validate(Object target, Errors errors) {

            UserDTO vm = (UserDTO) target;

            if (vm.getEndDate() != null) {
               if (vm.getStartDate().after(vm.getEndDate())) {
                errors.rejectValue(START_DATE, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
               }

               if (vm.getEndDate().equals(vm.getStartDate()) || vm.getEndDate().before(vm.getStartDate())) {
                errors.rejectValue(END_DATE, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
               }
            }

            if (vm.getCount() < 1) {
             errors.rejectValue(COUNT, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
            }
            .....

        }

    }


public class UserController {
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new UserDTOValidator());
    }
    @PostMapping()
    public ResponseEntity<UserDTO> create(@RequestBody @Valid UserDTO userDTO) {
       .....
    }
    .....
}

Then there is the business logic validation. For example: the @Entity User's startDate must be after some event occurred and the count has to be greater than some X if the last created User's birthDay is in Summer, in other case, the entity should be discarded by the User service.

@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private SomeEventService someEventService ;

    @Override
    public User create(User entity) {
        String error = this.validateUser(entity);
        if (StringUtils.isNotBlank(error)) {
            throw new ValidationException(error);
        }

        return this.userRepository.save(entity);
    }
    ....
    private String validateUser(User entity) {
        SomeEvent someEvent = this.someEventService.get(entity.getName()); 
        if (entity.getStartDate().before(someEvent.getDate())) {
            return "startDate";
        }
        User lastUser = this.userRepository.findLast();
        ....
    }

}

However I feel like this is not the best approach to handle business logic validation. What should I do? ConstraintValidator/HibernateValidator/JPA Event listeners? Can they work at @Entity class level or I have to create X of them for each different field check? How do you guys do it in a real production application?

Upvotes: 5

Views: 4305

Answers (1)

Abhishek Chatterjee
Abhishek Chatterjee

Reputation: 2049

In my suggestion,

  1. Use classic field level validation by @Valid

sample

void myservicemethod(@Valid UserDTO user)
  1. For custom business level validation in entity level, create validate method in DTO itself

sample

class UserDTO {
    //fields and getter setter
    void validate() throws ValidationException {
        //your entity level business logic
    }
}

This strategy will help to keep entity specific validation logic will be kept within the entity

  1. If still you have validation logic that requires some other service call, then create custom validation annotation with custom ConstraintValidator (eg. question on stackoverflow). In this case, my preference will be to invoke UserDTO.validate() from this custom validator in spiote of calling from service

This will help to keep your validation logic separated from service layer and also portable and modular

Upvotes: 3

Related Questions