Skip to main content

Webflux - Starting your journey

Webflux - Reactive Programming

This is the first article in a journey to create your first Webflux app. Webflux has been around for a few years and is widely used for asynchronous development. However, traditional training for engineers often starts with a synchronous paradigm, as many find it easier to teach. Asynchronous programming is usually treated as a secondary study, often overlooked and unfamiliar to many professionals. The goal of this series of articles is to understand how a framework like Webflux works. So, we'll start by understanding what we consider a prerequisite, which is reactive and functional programming. These paradigms offer innovative approaches to building highly responsive, resilient, and scalable applications that meet the increasingly demanding expectations of today's users.

Reactive programming

Reactive programming is the term used to describe systems that adhere to the pattern of software that is defined by the Reactive Manifesto and are characterized as applications that are highly responsive, resilient, elastic, and message-driven. The manifesto was created in response to increasing user expectations for millisecond response times and high availability.

Reactive programming is a paradigm for developing software that is non-blocking and asynchronous, which differs significantly from traditional software development approaches. It provides a specification for dealing with asynchronous streams of data, offering tools for transforming and combining streams and managing flow control. This makes it easier to reason about the overall program design.

For instance, in traditional programming, when saving results to a file or database, the application typically waits for the operation to complete, blocking execution during that time. However, non-blocking programming allows the application to continue executing other tasks while the write operation to the file or database is completed asynchronously.

Functional Programming

Before we move forward on more details of Webflux and Reactive Programming, it's important that we understand Functional Programming, a paradigm utilized in numerous programming languages that has gained popularity in recent years. In essence, Functional Programming is a coding style that emphasizes the use of functions. These functions take inputs, perform calculations without any side effects, and consistently return the same output for the same input.

// Lambda expression for a more compact function definition
public static boolean isEven(int number) ->  number % 2 == 0;

public static void main(String[] args) {
  // Use functional operations on a list
  List<Integer> numbers = List.of(1, 2,3, 4, 5);

  // Filter even numbers using lambda
  List<Integer> evenNumbers = numbers.stream()
      .filter(isEven)
      .toList();
  // Print the results
  System.out.println("Even numbers: " + evenNumbers);
}

The example provided above demonstrates functional programming principles. In this example, the function `isEven` accepts a number as an input and consistently returns the same value without relying on any global variables. This illustrates a core concept of functional programming called Pure Functions, where functions operate solely based on their inputs, without altering external state or relying on external factors. Pure Functions are widely adopted by developers and can be used everywhere, but they are especially crucial in the Webflux context. Here, operations can be executed by multiple threads simultaneously, so it's essential to ensure the correct context and avoid concurrency issues. 

In Java, encapsulation is recommended as the core of object-oriented programming. It encourages hiding an object's internal state and only exposing what is necessary to modify it. Methods that modify internal state are not considered pure functions.

One technique used in functional programming is immutability, which ensures that an entity cannot be modified after initialization. Java provides some immutable types, as well as the final keyword, to support immutability. Utilizing immutability requires using one or more constructors for initialization as needed and only providing accessor methods without any side effects.

public static ImmutableUser {
  private final String name;
  private final int age;
  private final ImmutableAddress address;

  public ImmutableUser(final String name, final int age){
    this.name = name;
    this.age = age;
  }

  public ImmutableUser(final String name, final int age, final = ImmutableAddress address){
    this.name = name;
    this.age = age;
    this.address = address;
  }

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

  public int getAge(){
    return this.age;
  }

  public ImmutableAddress getAddress(){
    return this.address;
  }
}

Another technique that is often used in functional programming is referential transparency, which means that a program or expression is considered referentially transparent if it can be replaced with its equivalent output value anywhere in the program without affecting the program's behavior or final result. Follow an example often used that also includes the use of immutable objects.

public static List<Integer> reverseList(List<Integer> list) {
  return new ArrayList<>(Collections.reverseOrder()).stream()
      .map(list::get)
      .collect(Collectors.toList());
}

This function takes an immutable list (list) and returns a new, reversed list. It doesn't modify the original list, adhering to referential transparency.

Java Function Interface

Java offers an interface to build functions and it's a key component that was introduced on Java 8 and frequently used when implementing a Webflux application. Going back to our definition of functional programming and techniques used on Java, we've studied that a function always receives a parameter that is used for calculation and a result is provided on that parameter only. This parameter can be a value or another function, that is used as argument or return values of other functions, this is also called first-class citizens.

That java.util.function.Function is an interface that takes an input and produces an output. It can be used to define new functions or to compose existing functions. If you are not familiar with this, I imagine that this is starting to be a little too abstract at this point, but this will help to have a more concise and expressive code. We haven't expressively talked about Java lambdas in this article, if that is also something that you are not familiar with it is worth pausing here and doing some additional reading, as they will contribute to having an expressive and easy-to-read code. 

In the example below we have an example of a java.util.function.Function.

import java.util.function.Function;

public class SimpleFunctionExample {
  public static void main(String[] args) {
    // Define a function that squares an integer
    Function<Integer, Integer> squareFunction = x -> x * x; // Lambda expression
    // Apply the function to different inputs
    int result1 = squareFunction.apply(5);

    System.out.println("Square of 5: " + result1); // Output: 25 
 }
}

The signature of the function squareFunction indicates that a function receives an Integer and returns an Integer, Function<Integer, Integer>. The first parameter represents the input and the second parameter the results type. You can also notice that the function is using a Lambda expression. To execute the function we use the method .apply() which takes an argument of a specified type and returns a result of a specified type.

 

Function Composition and Chaining

Function chaining is a technique that involves the composition of multiple functions into a single pipeline. As the name suggests, we can chain multiple steps and it's often used for data manipulation, where the output of a function it's the input to the next function. Let's add a new function on the previous example to calculate the double of the input value.

Function<Integer, Integer> doubleFunction = x -> x * 2;

Then we can use that same function to chain to the previous example.

Function<Integer, Integer> chainedFunction = squareFunction.andThen(doubleFunction);

// Apply the chained function in one step
int result = chainedFunction.apply(5);

System.out.println("Square and then double of 5: " + result); // Output: 50

We define separate functions for squaring (squareFunction) and doubling (doubleFunction) using lambda expressions. We use the andThen method to chain the functions. This method takes another Function as an argument and returns a new Function that applies the current function followed by the provided function. The chained function (chainedFunction) now combines the behavior of both individual functions: square the input and then double the result. We apply the chainedFunction to the value 5 in one step, obtaining the final result.

We've discussed a few techniques for functional programming, but now that we've also spoken about the Java Function Interface we can add one more technique often used when developing applications using webflux. The Java library will offer two main interfaces, Function and BiFunction, where the first receives 1 parameter and the second receives 2, both still return a single value. That might introduce limitations to functions that use more than 2 parameters as input, so we need to use Currying. Which is a mathematical technique of converting a function that takes multiple arguments into a sequence of functions that take a single argument. It gives us a powerful composition technique where we don’t need to call a function with all its arguments.

import java.util.function.Function;

public class CurryingFunctionExample {

  public static void main(String[] args) {

    // Curried function that takes a multiplier as input
    // and returns a function that multiplies a number by that multiplier
    Function<Integer, Function<Integer, Integer>> curriedMultiplier = multiplier -> x -> multiplier * x;

    // Create a function that doubles a number using currying
    Function<Integer, Integer> doubleFunction = curriedMultiplier.apply(2);

    // Create a function that triples a number using currying
    Function<Integer, Integer> tripleFunction = curriedMultiplier.apply(3);

    // Apply the curried functions
    int doubled = doubleFunction.apply(5);
    int tripled = tripleFunction.apply(5);

    System.out.println("Doubled: " + doubled); // Output: Doubled: 10
    System.out.println("Tripled: " + tripled); // Output: Tripled: 15

    // References:
    // - Currying: https://en.wikipedia.org/wiki/Currying
    // - Functional Programming in Java: https://www.baeldung.com/java-functional-programming
  }
}

So far, we've reviewed the concepts of reactive and functional programming, as well as the Java Function Interface, which is crucial for composing and chaining pipelines. These are some of the main concepts that will help us understand and develop Webflux applications. In our next article, we'll delve into the Webflux framework and explore the components that make up an application.

Post by Ricardo Corassa
March 19, 2024

Comments