Good article.
Maybe you could look up the Try monad API (Scala or Vavr works in Java + Kotlin), by using some extra helper methods you can have something probably a little bit lighter to use.
I believe your example would look like the following with the Try monad (in Java):
public UserDTO register(UserRegistrationRequest registrationRequest) {
return Try.of(() -> authService.userExists(registrationRequest.email))
.filter(Objects::isNull, () -> badRequest("user already exists"))
.map(userId -> authService.register(registrationRequest.email, registrationRequest.password))
.get();
}
The login() function would be using the same pattern to call authService.verify() then filtering nonNull and signing the JWT, so it would be the same pattern for both.