Getting started with WebFlux - Part 1: Functional Java

Learn some common functional programming concepts applied to Java

Published: Aug. 12, 2021

Introduction

Nowadays, we can find a lot of options out there if we want to start a web server application. But what should we take into account when picking an option? The language we are going to be writing? Available libraries and community engagement? Performance under high concurrency? Let's face it: As software developers, we want it all!

NodeJS has been a popular choice over the last few years. And why not? We'd be writing JavaScript, which can be enhanced with TypeScript, there's a lot of open-source libraries available in NPM with a great community of contributors, and most importantly, its event loop, which allows performing non-blocking I/O operations even though JavaScript is single-threaded.

This sounds great, but what if we want more? JavaScript type safety is not the strongest even with TypeScript, and the libraries/frameworks available are sometimes not mature enough. Taking into account these facts, my first thought was: What about Java? It's a strongly typed language, with a huge community and most of the libraries/frameworks are very mature. The only drawback: Java I/O operations are blocking. Or they were until Reactive Streams for Java were introduced, allowing non-blocking I/O operations and data processing.

During this series of blog posts, we’ll see how to work with Reactive Java to create a web server application. Going from the functional approach of Java, to Spring WebFlux, the Project Reactor, the R2DBC project, Reactive Spring Security, and many other features.

Spring WebFlux

A lot of libraries/frameworks started to add reactivity to their stacks, including the powerful Spring Framework. To make things even better, and similar to other tools like NodeJS, Spring WebFlux was released as part of the Spring Boot project. This brings all of the power and abstractions of Spring Boot to a non-blocking, light-weight web framework which is even capable of running without a Java Application Server with the help of Netty, as we'll see further in this series.

To work with Reactive Java we'll need to leave behind the traditional imperative programming paradigm that has strongly characterized Java for so many years, and embrace the modern functional programming (FP) paradigm in Java 8 and above. To start ourselves on WebFlux, we'll first go through each FP principle and learn how to apply it in Java.

Functional Programming Principles in Java

1. Pure Functions

Purity is one of the main features of Functional Programming. It means we should not have any side effects from memory or I/O. We should always get the same result from a function for the same input.

public Integer sum(final Integer a, final Integer b) {
  return a + b;
}

Nothing in this function has side effects. If it's invoked as sum(3, 2), the result will always be 5. We should never run other operations from it or mantle with its expected behavior:

public Integer sum(final Integer a, final Integer b) {
  return new Random().nextInt() + a + b;
}
public Integer sum(final Integer a, final Integer b) {
  this.launchMissiles();
  return a + b;
}

Both examples above add side effects to the sum(..) function. For the first one, invoking sum(3, 2) will always give different results. For the second, another operation will be running within the function even if the output is constant for the same input. Java does not have any built-in feature that prevents us from causing side effects, so we should always be aware to avoid this kind of situation.

2. Immutability

Immutability prevents unexpected side effects from spreading through our program. In short, this means that once the state is created, it should never change. Instead, we create a new state with the required changes. So instead of doing something like this:

var greeting = "Hello";

greeting += " world";

System.out.println(greeting); // Hello world

We should do something like this:

final var prefix = "Hello";

final var greeting = prefix.concat(" world");

System.out.println(prefix); // Hello

System.out.println(greeting); // Hello world

Notice the prefix variable hasn't changed at all. Instead, we've used the concat(..) method that returns a new String instance and assigns the result to the greeting variable. Something else to note is the final accessor, which prevents the variable from being changed once assigned. The final accessor might be compared to the JavaScript const keyword. We should be sure to use it on all variables, parameters and fields to avoid state mutation in our code.

This seems easy to achieve on primitives, but what about complex encapsulated objects? The use of setters is obviously out of the picture since those methods will effectively mutate the state of the instance. Additionally, using final in the declaration of its fields will prevent us from creating any setter method at all. Java does not have any built-in feature that allows us to change an object's fields and return a new instance in the process, but we can overcome this using some nice design patterns.

The Builder Pattern

One might think our best option would be to use the builder pattern, which is becoming a very popular solution in fluent APIs designs. However, this pattern technically still involves state mutation in the builder's implementation details. Inside the builder, fields mutate to different states until the object is finally built into a new instance. For some this pattern could be enough to tackle immutability, so let's see an example of how to apply it:

import java.time.LocalDate;
import java.util.Optional;

public final class Person {

  private final String name;

  private final @Nullable LocalDate birthdate;

  public Person(final @Nullable String name, final @Nullable LocalDate birthdate) {
    this.name = Optional.ofNullable(name).orElse(“”);
    this.birthdate = birthdate;
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }

  public static PersonBuilder builder() {
    return new PersonBuilder();
  }

  public PersonBuilder toBuilder() {
    return new PersonBuilder(this);
  }

  public static class PersonBuilder {

    private String name; // Notice these fields cannot be final

    private LocalDate birthdate; // Notice these fields cannot be final

    public PersonBuilder() {
    }

    public PersonBuilder(final Person person) {
      this.name = person.getName();
      this.birthdate = person.getBirthdate();
    }

    public Person build() {
      return new Person(this.name, this.birthdate);
    }

    public PersonBuilder name(final String name) {
      this.name = name;
      return this;
    }

    public PersonBuilder birthdate(final LocalDate birthdate) {
      this.birthdate = birthdate;
      return this;
    }
  }
}

These patterns add a lot of boilerplate to our classes, but fortunately, we can use Project Lombok in our project and IDEs. This will simplify our code to this:

import java.time.LocalDate;

import lombok.Builder;
import lombok.Builder.Default;
import lombok.Getter;

@Getter
@Builder(toBuilder = true)
public final class Person {

  private final @Default String name = "";

  private final @Nullable LocalDate birthdate;
}

Lombok is an annotation-based library that modifies the compilers' AST to "generate" code into your classes at compile-time. So the second implementation will be the same as our first (i.e. the second can be "delomboked" into the first). The @Builder(toBuilder = true) annotation will create the PersonBuilder inner class (including the toBuilder() method) and the @Getter annotation will create “getter” methods for all fields. The only downside of Lombok is that it hides the implementation details from us, so we should try not to abuse it, or better, get very familiar with the annotations and the details of the code they generate.

Finally, we can create an object and change its field values like this:

final var john = Person.builder()
  .name("John")
  .birthdate(LocalDate.now())
  .build();

final var johnDoe = john.toBuilder()
  .name("John Doe")
  .build();

john.getName() // John

johnDoe.getName() // John Doe

john.getBirthdate().equals(johnDoe.getBirthdate()) // true

Notice that john is never mutated. Instead, we create another object johnDoe based on the john instance.

The Wither Pattern

Since we already established that the Builder Pattern still involves state mutation, we need to find a better way to change the state of an object while creating a new instance in the process. The "wither" pattern ,also known as immutable setters, can help us replace the commonly known setters with immutable versions of them. The idea is simple: a mutable setter method changes the state of the field to the provided value and returns nothing, while an immutable setter creates a new instance with the provided value and returns that instance instead of nothing. We call them "withers" because instead of prefixing the methods with "set" we use the "with" word. E.g. setName(..) becomes withName(..).

To use "withers" a constructor with all the arguments is always required. The reason is simple: If we don't have this constructor, there's no possible way of creating a new instance for each field we need to create a "wither" for. Following the Person example above, this pattern can be implemented like this:

public final class Person {

  private final String name;

  private final @Nullable LocalDate birthdate;

  private Person(final @Nullable String name, final @Nullable LocalDate birthdate) {
    this.name = Optional.ofNullable(name).orElse("");
    this.birthdate = birthdate;
  }

  public static Person init() {
    return new Person(null, null);
  }

  public String getName() {
    return this.name;
  }

  public Person withName(final String name) {
    return new Person(name, this.birthdate);
  }

  public LocalDate getBirthdate() {
    return this.birthdate;
  }

  public Person withBirthdate(final @Nullable LocalDate birthdate) {
    return new Person(this.name, birthdate);
  }
}

Notice that using this pattern we never change the value of any field, as they are effectively final, but we can create a new instance with a different value for each one of them. Making the constructor “private” can be considered a good practice because constructors rely on positional arguments to initialize values. This is prone to errors since it is hard to know which value is being assigned to each field, especially with many arguments. Using initializers and “withers” on the other hand, are explicit of which field a value is being assigned to, so we've also added a convenience init() method to create an instance of the object with its default values. From that initial instance, we can use the "wither" methods to create an object with custom values.

This pattern also involves a lot of boilerplate. This may look fine with only two fields, but it won't be such a trivial task when we have a class with a lot of fields. The good thing is that Lombok has us covered here too. However, to achieve this same implementation with Lombok, we'll need more than one annotation. Let's see how this looks like first:

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Value;
import lombok.With;

@With
@Value
@Builder(access = AccessLevel.PRIVATE)
public class Person {

  private @Default String name = "";

  private @Nullable LocalDate birthdate;

  public static Person init() {
    return Person.builder().build();
  }
}

Let's start by reviewing Lombok's annotations here. @With will create the "wither" methods for each field in the class, while @Value will make our class immutable. It will add final to the class and all of its fields, create getter methods for them, and will also create a constructor with all the arguments. But wait a minute, what's the @Builder doing there?

The builder here is just a workaround to be able to add default values to the constructor. At the moment the @Default annotation only works with the @Builder (the annotation is actually @Builder.Default). That's why we make this builder private; we just want to use it to create the object with its default values. There's an ongoing discussion on Lombok's GitHub site to consider adding a @Value.Default or @With.Default annotation so we can avoid this workaround. Go take a look and add your support to the cause. That's always helpful!

Finally, let's see how we'll use this immutable object:

final var john = Person.init()
  .withName("John")
  .withBirthdate(LocalDate.now());

final var johnDoe = john.withName("John Doe");

john.getName() // John

johnDoe.getName() // John Doe

john.getBirthdate().equals(johnDoe.getBirthdate()) // true

As you can see, john hasn't changed as we created johnDoe by modifying it. And unlike the builder approach, we removed the overhead of invoking the .build() method at the end to create the object.

3. Referential Transparency

This is more of a conceptual principle. We can say a function is referentially transparent if it consistently returns the same result for the same data input. So to put it simply:

Pure Functions + Immutability = Referential Transparency

4. First-class and higher-order Function

First-class functions mean we can treat a function as a first-class citizen, supporting all the operations available to other entities (being passed as an argument, returned from a function, modified, and assigned to a variable). This means we have high-order functions, which can receive functions as arguments and/or return a function as a result.

With this concept clear, the first thing that may jump to our mind could be: "but wait... Java does not have functions, only methods!". That was true until Java 8 where @FunctionalInterface was introduced in conjunction with lambda expressions (which is syntax sugar to implement interfaces anonymously).

The @FunctionalInterface annotation

A functional interface is not so different from a classic Java interface. The difference is that we need to apply the @FunctionalInterface annotation and that it should have exactly one abstract method.

@FunctionalInterface
public interface IntegerFunction {

  public Integer apply(Integer num);
}

Simple enough, right? Before Java 8 we would have to implement this interface in a class, create a new instance, and call the method:

public class Add10Impl implements IntegerFunction {

  @Override
  public Integer apply(final Integer num) {
    return num + 10;
  }
}

final var add10 = new Add10Impl();
final var result = add10.apply(5); // 15

Lambda expressions

The above implementation is very tedious, and we would have to create more classes if we wanted to add more implementation. A workaround would be to assign the implementation to a variable declaring an anonymous implementation of the interface:

final IntegerFunction add10 = new IntegerFunction() {

  @Override
  public Integer apply(final Integer num) {
    return num + 10;
  }
}

This should work, but we can make it better. We can syntax sugar this using a lambda expression:

(Type1 arg1, Type2 arg2, ...) -> { /* implementation */ }; // Complete syntax

(arg1, arg2) -> returnValue; // simplified syntax

arg -> returnValue; // simplified syntax with a single argument

This way we can implement some IntegerFunction functions in a more compact and readable way:

final IntegerFunction addTen = (Integer x) -> { return x + 10 };
addTen.apply(5); // 15

final IntegerFunction subtractFive = x -> x - 5;
subtractFive.apply(12); // 7

As simple as that, we now have "functions" in Java, which can be passed as parameters or returned from other functions. We also said Java used to have only methods, so what if we also want to use them as first-class citizens? We can, but we'll need a slightly different syntax. Instead of accessing the methods with the . operator, we should refer to them using the :: operator. This is known as method reference:

private void greeter(Supplier<String> supplier) {
  final var name = supplier.get();
  System.out.println("Hello, my name is " + name);
}

// johnDoe.getName() signature is `public String getName() { .. }` which fulfills the `Supplier` function type
greeter(johnDoe::getName); // Hello, my name is JohnDoe

// the Person constructor signature is `Person(String name, Date birthdate) { ... }` which fulfils the type of `BiFunction<String, Date, Person>` function type
final BiFunction<String, Date, Person> createPerson = Person::new;
createPerson.apply("Peter", new Date()); // new Person object

Generic types

Functions are usually a generalization, so it makes sense to think that we wouldn’t know the types of the functional interface until we implement it. Java gives us the ability to use generic types to work our way into function generalization. As an example we can write a functional interface that will map one value to another:

@FunctionalInterface
public interface MapFunction<T, R> {

  public R apply(T t);
}

We don't really know which type will be mapped to which or how, so in MapFunction<T, R> we represent the input type as T and the return type as R. Then we can implement different mappers for different types:

final MapFunction<Integer, String> mapToCurrency = value -> "$" + value;
final var bill = mapToCurrency.apply(5000); // "$5000"

final MapFunction<String, String[]> mapToWords = phrase -> phrase.split(" ");
final var words = mapToWords.apply("Hi! My name is John"); // ["Hi!", "My", "name", "is", "John"]

Generalization is common in functional programming, therefore Java has given us some built-in generic functional interfaces:

Predicate<T>          // (T t) -> boolean;

Consumer<T>           // (T t) -> void;

Supplier<T>           // () -> T;

Function<T, R>        // (T t) -> R;

UnaryOperator<T>      // (T t) -> T;

BiFunction<T, U, R>   // (T t, U u) -> R;

BinaryOperator<T>     // (T t1, T t2) -> T;

// Some more variations...

// And the implementation of the identity function
Function.identity();  // (T t) -> t;

Stop procedural patterns, embrace Stream<T>

After all this information you might be thinking "Ok but, how do I avoid mutation when working with arrays and collections?". Working with arrays and collections (List<T>, Map<T>, etc.) will inevitably result in the use of imperative loops with state mutation. This is because arrays and collections are inherently mutable and they allow that with the help of built-in methods like .add(..), .remove(..), .set(..), .put(..), .replace(..), etc. This is what we call procedural design patterns, and if we want to do functional Java, we'll need to completely avoid them:

/**
 * IMPORTANT: The following are examples of procedural patterns that we should avoid
 */

final int[] intArray = {1, 2, 3, 4, 5};

for (int i = 0, i < intArray.length; i++) {
  // This is possible even though intArray is final :(
  intArray[i] = intArray[i] * 2;
}

final List<Integer> intList = new ArrayList({-1, 5, -4, 6, 2});

final List<Integer> intList = new ArrayList<>();
intList.add(-1);
intList.add(3);
intList.add(-4);
intList.add(6);
intList.add(2);

// Oh no! Not this pattern :(
for (int i = intList.size() - 1; i >= 0; i--) {
  if (intList.get(i) > 0) {
    intList.remove(i);
  }
}

Ok so, we said that working with arrays and collections will inevitably result in mutations, but that's not entirely true! Arrays can always be mutated so we'll see how we can overcome that later, but some time ago Java introduced the concept of immutable collections, which we can get by simply using the built-in static factory methods (List.of(..), Map.of(..), etc.). Trying to modify immutable collections will result in a UnsupportedOperationException thrown at runtime, so how can we transform and process these collections?

There is where the Stream<T> API comes into the picture. We can transform arrays and collections into Stream<T>, use its API to transform and process the data, and then collect the data back into an array or collection. We do this with the functionally popular operators of .map(..), .filter(..), .reduce(..), etc. Diving deep into Java Streams deserves its own article, so let's just see some examples of how to use them and keep doing functional programming on Java.

final var intArray = new int[] {1, 2, 3, 4, 5};

final var duplicates = Arrays.stream(intArray)
  .map(num -> num * 2)
  .toArray();
// {2, 4, 6, 8, 10}

final var onlyPositives = List.of(-1, 3, -4, 6, 2)
  .stream()
  .filter(num -> num > 0)
  .toList();
// [3, 6, 2]

final var addition = Stream.of(1, 2, 3, 4, 5)
  .reduce((acc, num) -> acc + num)
  .get();
// 15

The Java Stream API provides lots of methods that will fit our needs. For now, we can know that we can use them to manipulate data of immutable arrays and collections functionally.

No more Null Pointer Exceptions - Optional<T>

Now that we have the basics for Java functional programming, let's dive into a feature that allows us to better handle null references functionally. Null references are usually the source of many problems as they’re intended to denote the absence of a value. As Java developers, we may all have experienced the nightmare of java.lang.NullPointerException at some point when we try to reference a method/property from a null instance. To handle this exception we add if-statements (sometimes deeply nested) to check for null values. However, since the release of Java 8, a better (and more functional) solution was introduced with java.util.Optional.

Optional<T> is a container object that may or may not contain a non-null value of the parameterized type T. If the value is not present then we'll have an empty Optional instance. The functional approach comes when we start handling and transforming the contained value. The Optional class has a bunch of methods that take functions in their arguments (i.e. map, flatMap, filter, etc.), which we can use to handle the value in many ways.

final Optional<String> foo = Optional.of(foo);

final Optional<String> foo = Optional.ofNullable(arg1); // arg1 may or may not be null

// Given arg1 has a property `bar` which has a property `baz`
final Integer baz = Optional.ofNullable(arg1)
  .map(arg1 -> arg1.getBar())
  .map(bar -> bar.getBaz())
  .or(() -> arg2);

// Check if value is present inside the container
if (foo.isPresent() && !foo.isEmpty()) {
  System.out.println(foo);    // Optional<String>[foo]
  System.out.println(foo.get());  // We can unwrap the value and get “foo”
}

final List<Integer> nums = Arrays.asList(1, 5, -3, 2, 7, -1, -6, 9);

// Works with collections
final List<Integer> positives = Optional.of(nums)
 .filter(n -> n >= 0)
 .orElse(new ArrayList<Integer>()); // alternative if the output of the filter is "empty"

Class Person {
 private Optional<String> name;
 // constructors
 // getter...
}

// Works with complex objects
final Optional<String> optionalName = Optional.ofNullable(person)
 .map(person -> person.getName()); // person.getName() will return an Optional<String>

// We can flatten the transformation if the returned value is an Optional
final String name = Optional.ofNullable(person)
 .flatMap(person -> person.getName());

It's a good idea to get used to Optional<T> as it will help us to better understand the reactive containers Mono<T> and Flux<T> which WebFlux is based on. We'll go deeper into these containers in the next article, but for now, let's think of them as containers that will help us manage data flow streams in reactive applications.

A functional router teaser with WebFlux

We've seen a lot of new concepts in this post, but don't worry, they will all help create a knowledge base to help work with functional/reactive applications. To wrap everything up, let's go through a small example of how a WebFlux router will look like. This is something we’ll be looking at in more detail in the next post of this series, but it helps us get a good idea of how useful it is to make Java functional:

@Configuration
public class Router {

  @Bean
  public RouterFunction<ServerResponse> apiRouter(final PersonHandler personHandler) {
    return route()
      .path("/api", () ->
        route(
          .GET("/say-hello", request ->
            Mono.justOrEmpty(request.queryParam("name"))
              .map("Hello %s!"::formatted)
              .switchIfEmpty(Mono.just("Hello world!"))
              .flatMap(ok()::bodyValue)
          )
          .path("/person", () ->
            route()
              .GET("", personHandler::getAll)
              .GET("/{id}", personHandler::getOne)
              .POST("", personHandler::save)
              .PUT("", personHandler::update)
              .DELETE("/{id}", personHandler::delete)
              .build()
          )
          .build()
        )
      )
      .build();
  }
}

Notice how this functional router uses lambda expressions and method references to add handlers for each route. We can also see the use of .map and .flatMap which are common ways to manipulate data when we work with functional programming and Reactive Streams. It's also worth mentioning that these functions are pure and the router is free of side effects.

Conclusion

In this post we have worked our way into functional programming using Java 8+ features. This will open the path to Reactive Streams which is the base for the Reactor Project on which WebFlux is built. In the next part of this series we'll be seeing how to start our WebFlux project, set up a reactive database connection bound to Java models, and reactive repositories.

Written by


José Luis León

Written by

José Luis León