September 15, 2019

Vavr Functional Programming Context Pattern

FP pattern with Vavr & Java

If you don’t know Vavr:

Vavr is a functional library for Java. It helps to reduce the amount of code and to increase the robustness. A first step towards functional programming is to start thinking in immutable values. Vavr provides immutable collections and the necessary functions and control structures to operate on these values. The results are beautiful and just work.

It contains implementations of Monads, Functor and plenty of FP stuff.

Try

I won’t bother writing a long tutorial on how to use Vavr because you’ll find some pretty good one on the net, but I’ll explain a pattern I’ve come with for using Vavr’s Try effectively when my services are working like pipelines, meaning:

  1. data in
  2. transform (may have side effect)
  3. data out
  4. transform (may have side effect)
  5. data out
  6. transform (may have side effect)
  7. data out

But what if during step 6 we need data which was computed during step 2?

We need to keep a context of the pipeline which will be passed during the execution of the pipeline.

Of course you don’t want to execute step 4 if step 2 failed.

Using Lombok, it’s really easy to create Java beans enriched with plenty of features which makes the code easy to both write and read, this is our context.

Example

Let’s take a look at some sort of registration pipeline where we can imagine the business is the following:

  1. Given an id
  2. Retrieve a user details: email, first name and password (not really secure ;-)
  3. Register an account on Twitter
  4. Authenticate on Twitter
  5. Tweet Hello, world
  6. Update the user details with the Twitter account id
  7. Log something in case of success
  8. In case of error anywhere log something also

Simple code

A simple code example for this could be:

@RequiredArgsConstructor
class TwitterRegistrationService {
    final UserService userService;
    final TwitterService twitterService;
    final BusinessLogger blog;
    
    /**
     * Register the given user on Twitter,
     * post a hello world and return this tweet URL.
     *
     * @param userId the user id
     * @return the tweet URL
     */
    public String register(String userId) {
        try {
            User user = userService.byId(userId);
            if (user == null) {
                blog.logErrorRegisteringTwitterAccount(id);
                return null;
            }
            
            TwitterAccount account = twitterService.register(user.email, user.firstName, user.password);
            if (account == null) {
                blog.logErrorRegisteringTwitterAccount(id);
                return null;
            }
            
            String authToken = twitterService.authenticate(user.email, user.password);
            if (authToken == null) {
                blog.logErrorRegisteringTwitterAccount(id);
                return null;
            }
            
            Tweet tweet = twitterService.tweet(authToken, "Hello, world!");
            if (tweet == null) {
                blog.logErrorRegisteringTwitterAccount(id);
                return null;
            }
            
            userService.updateTwitterAccount(userId, account.id);
            
            blog.logSuccessRegisteringTwitterAccount(id);
            
            return tweet.url;
        } catch (Exception e) {
            blog.logErrorRegisteringTwitterAccount(id, e);
            return null;
        }
    }
}

This code is easy to follow but has a lot of repetitions, first we could return Options so that the code don’t have to check for null.

Improved code

Let’s see how we can improve the code by just modifying the RegistrationService and creating the context object.

Context

The context needs to store anything which is useful for the pipeline to execute:

@Data
@Accessors(chain = true)
class Context {
    String id, email, firstName, password;
    String accountId, token, url;
    
    public Context(User user) {
        this.id = user.id;
        this.email = user.email;
        this.firstName = user.firstName;
        this.password = user.password;
    }
}

The Lombok annotations will help writing cleaner and concise code. Of course you could also use MapStruct so that it can generate a Mapper from User to Context and avoid some manual code ;).

RegistrationService

@RequiredArgsConstructor
class TwitterRegistrationService {
    final UserService userService;
    final TwitterService twitterService;
    final BusinessLogger blog;
    
    // Create a registration context based on the userId
    Try<Context> userDetails(String userId) {
        return Try.of(() -> userService.byId(userId)).map(Context::new);
    }
    
    // register a twitter account for the user
    Try<Context> registerTwitterAccount(Context c) {
        return Try.of(() -> twitterService.register(c.email, c.firstName, c.password))
            .map(account -> c.setAccountId(account.id));
    }
    
    // authenticate on twitter as the newly created user
    Try<Context> authenticate(Context c) {
        return Try.of(() -> twitterService.authenticate(c.email, c.password))
            .map(c::setToken);
    }
    
    // tweet "Hello, world!" and retrieve the tweet URL
    Try<Context> tweet(Context c) {
        return Try.of(() -> twitterService.tweet(c.token, "Hello, world!"))
            .map(tweet -> c.setUrl(tweet.url));
    }
    
    void updateUserTwitterAccount(Context c) {
        return Try.run(() -> userService.updateTwitterAccount(c.id, c.accountId));
    }
    
    /**
     * Register the given user on Twitter,
     * post a hello world and return this tweet URL.
     *
     * @param userId the user id
     * @return the tweet URL if any
     */
    public Option<String> register(String userId) {
        return userDetails(userId)
            .flatMap(this::registerTwitter)
            .flatMap(this::authenticate)
            .flatMap(this::tweet)
            .andThen(this::updateUserTwitterAccount)
            .andThen(c -> blog.logSuccessRegisteringTwitterAccount(userId))
            .onFailure(e -> blog.logErrorRegisteringTwitterAccount(userId, e))
            .map(Context::getUrl)
            .toOption();
    }
}

Almost the same amount of code (around 50 lines), but now the pipeline is clear, you can clearly see what registering is about.

Besides, you avoid a big try catch and tell clearly to the consumer of your RegistrationService that register may fail and return None. By the way I prefer using Vavr’s Option to Java’s Optional.

You could return a Try also but in this case any error is correctly logged (imagine going into ELK) so there’s no need for the caller to know exactly what went wrong, just that it went wrong.

This pattern

I came with this pattern which plays really well with Vavr & Lombok APIs when I started to use Vavr a lot, I don’t know if it has a name except (and I don’t care that much :-), it’s pure pragmatism, that’s what I would have done if I was to use Clojure at work, just passing a map from step to step, associng keys in it. However in Java it’s more practical to define and use a custom object than using a Map<String, Object>, that’s where Lombok comes just as needed to reduce the boilerplate.

I’ve presented this pattern at work during katas and presentation of Vavr and how to do Functional Programming in Java, and we’ve been using it for some time now, it powers some complex pipelines in our microservices and I believe make them both safer and more maintenable.

Maybe it’ll be of interest to someone.

Cheers!

Alexandre Grison - //grison.me - @algrison