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.