Enterprise Java

Convert Mono Object to Another Mono Object in Spring WebFlux

Spring WebFlux is a reactive programming framework designed for handling asynchronous and non-blocking operations efficiently. Unlike traditional Spring MVC, which relies on blocking I/O, WebFlux enables applications to process multiple requests concurrently without tying up server resources. At the core of WebFlux’s reactive programming model are Mono<T> and Flux<T>, which represent single and multiple reactive data streams, respectively. In real-world applications, there is often a need to transform one Mono<T> into another Mono<R>. This can be useful for tasks such as data mapping, service calls, enriching responses, and merging data from multiple sources. Choosing the right transformation technique is crucial for maintaining a clean, efficient, and maintainable reactive pipeline. In this article, we will explore different ways to convert a Mono object in Spring WebFlux using practical examples.

1. Setting Up a Spring Boot WebFlux Project

Before we dive into transformation techniques, let’s set up a Spring Boot WebFlux project. To use Spring WebFlux, add the following dependencies in pom.xml:

01
02
03
04
05
06
07
08
09
10
11
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
 
<!-- Reactor Test for Testing Mono -->
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
</dependency>

This setup includes:

  • spring-boot-starter-webflux.
  • reactor-test.

Spring Boot WebFlux provides all the necessary tools to build a fully reactive application. Reactor Test helps us verify the behavior of Mono transformations in a test-driven approach.

2. Defining the Model Classes

Before applying transformations, let’s define the model classes for our movie rental system. This article will use a real-world example using an online movie rental system, featuring:

  • RentalResponse: Represents the result of a rental operation.
  • Customer: Represents a customer renting movies.
  • Movie: Represents available movies.

Customer Class

The Customer class represents a person renting movies from the system.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class Customer {
     
    private String customerId;
    private String name;
    private String email;
    private boolean active;
 
    public Customer(String customerId, String name, String email, boolean active) {
        this.customerId = customerId;
        this.name = name;
        this.email = email;
        this.active = active;
    }
 
    // Getters and Setters
}

Here, the Customer class has an ID, name, email, and an active status indicating whether they are allowed to rent movies.

Movie Class

The Movie class represents a movie available for rental.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class Movie {
     
    private String movieId;
    private String title;
    private double price;
    private boolean available;
 
    public Movie(String movieId, String title, double price, boolean available) {
        this.movieId = movieId;
        this.title = title;
        this.price = price;
        this.available = available;
    }
    // Getters, Setters, equals and hashcode
}

This class defines a movie’s ID, title, price, and availability status.

RentalResponse Class

The RentalResponse class encapsulates the result of a rental operation.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class RentalResponse {
     
    private String customerId;
    private String movieId;
    private String status;
 
    public RentalResponse(String customerId, String movieId, String status) {
        this.customerId = customerId;
        this.movieId = movieId;
        this.status = status;
    }
 
    // Getters, setters , equals and hashcode
     
}

This class holds information about which customer rented which movie and the rental status.

3. Creating the Spring Boot Test Class

3.1 Transforming Mono Data Synchronously with map()

The example below highlights how map() is used in Spring WebFlux to transform Mono objects synchronously. It is a simple technique for modifying or extracting specific data from reactive streams without introducing any asynchronous behavior.

Transforming a Customer Object to CustomerDTO

01
02
03
04
05
06
07
08
09
10
11
12
@Test
void testMapTransformation() {
    Mono<Customer> customerMono = Mono.just(new Customer("CUST123", "Thomas", "tom@jcg.com", true));
 
    Mono<CustomerDTO> customerDTOMono = customerMono.map(customer
            -> new CustomerDTO(customer.getName(), customer.getEmail())
    );
 
    StepVerifier.create(customerDTOMono)
            .expectNext(new CustomerDTO("Thomas", "tom@jcg.com"))
            .verifyComplete();
}

This block of code is part of a Spring Boot test class that demonstrates a synchronous transformation of a Mono<Customer> object into a Mono<CustomerDTO> using the map() method. The test method testMapTransformation() verifies the transformation process using StepVerifier from Reactor Test to assert the expected outcome.

At the core of this transformation, we have two classes: Customer and CustomerDTO. The Customer class represents a customer entity with fields such as customerId, name, email, and active status. However, for certain API responses or UI representations, only the name and email fields might be required. The CustomerDTO class is designed to hold this simplified version of the customer information. The goal of this transformation is to extract only the necessary fields from Customer and store them in a CustomerDTO object.

Inside the testMapTransformation() method, a Mono<Customer> is created using Mono.just(), wrapping a Customer object. This represents a single customer record in a reactive pipeline. The transformation occurs with the map() method, which applies a synchronous function that takes a Customer object and converts it into a CustomerDTO. Since map() is synchronous, it immediately returns a new Mono<CustomerDTO> with the transformed data.

To ensure that the transformation is performed correctly, we use StepVerifier.create(customerDTOMono). This method initiates the reactive stream and checks whether the expected CustomerDTO object ("Thomas", "tom@jcg.com") is emitted. If the emitted object matches the expected output, the test passes. The verifyComplete() method ensures that the Mono completes successfully without emitting any errors.

3.2 Asynchronous Transformations with flatMap()

While map() is used for synchronous transformations, flatMap() is required when calling asynchronous services inside a transformation.

Fetching Movie Details Before Renting

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class MovieService {
    public Mono<Movie> getMovieById(String movieId) {
        return Mono.just(new Movie(movieId, "RoboCop", 19.99, true));
    }
}
 
    @Test
    void testFlatMapTransformation() {
        MovieService movieService = new MovieService();
        Mono<String> movieIdMono = Mono.just("MOV456");
 
        Mono<Movie> movieMono = movieIdMono.flatMap(movieService::getMovieById);
 
        StepVerifier.create(movieMono)
                .expectNext(new Movie("MOV456", "RoboCop", 19.99, true))
                .verifyComplete();
    }

This code demonstrates the use of asynchronous transformations in Spring WebFlux using the flatMap() method. It involves fetching a Movie object based on a given movieId by utilizing a MovieService class. The goal of this transformation is to convert a Mono<String> containing a movieId into a Mono<Movie> that holds the detailed information of the movie.

The MovieService class provides the method getMovieById(String movieId), which takes a movieId as an argument and returns a Mono<Movie>. For demonstration purposes, the method uses Mono.just() to return a pre-defined Movie object. In real-world applications, this method would typically interact with an external data source, such as a database or an external API, to fetch the actual movie details asynchronously.

Within the test method testFlatMapTransformation(), a Mono<String> is created using Mono.just("MOV456"), representing a reactive stream containing only the movie ID. The transformation occurs through the flatMap() method, which is crucial for handling asynchronous operations in a reactive pipeline. By passing the getMovieById method reference from MovieService into flatMap(), we initiate an asynchronous call to fetch the complete Movie object associated with the given movieId. The flatMap() method is preferred here over map() because flatMap() allows the inner Mono<Movie> returned by getMovieById to flatten into the reactive stream, thereby avoiding nesting of Mono objects.

3.3 Merging Data from Multiple Sources (zipWith())

The zipWith() method merges multiple Mono<T> instances to create a single result.

Combining Customer and Movie Data for Rental

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
void testMergingDataWithZipWith() {
    Mono<Customer> customerMono = Mono.just(new Customer("CUST789", "Bob", "bob@jcg.com", true));
    Mono<Movie> movieMono = Mono.just(new Movie("MOV987", "The Matrix", 12.99, true));
 
    Mono<RentalResponse> rentalResponseMono = customerMono.zipWith(movieMono,
            (customer, movie) -> new RentalResponse(customer.getCustomerId(), movie.getMovieId(), "Rented")
    );
 
    StepVerifier.create(rentalResponseMono)
            .expectNext(new RentalResponse("CUST789", "MOV987", "Rented"))
            .verifyComplete();
}

This block of code demonstrates merging data from multiple sources using the zipWith(). The primary goal of this transformation is to combine two separate Mono streams—one containing Customer information and the other containing Movie details—into a single Mono<RentalResponse> object that represents a successful movie rental transaction.

The test method testMergingDataWithZipWith() first creates two independent Mono objects: customerMono and movieMono. The customerMono contains a Customer object representing a user named Bob, while the movieMono holds a Movie object representing the movie “The Matrix.” Each of these Mono objects emits a single value and completes immediately. However, these two streams remain separate until they are merged.

The transformation takes place with the zipWith() method. This method allows us to combine the emissions of both Mono sources into a single new object. It takes two arguments: the movieMono as the second Mono source and a combining function that takes both emitted values (Customer and Movie) and produces a RentalResponse object. In this case, the function extracts the customerId from the Customer object and the movieId from the Movie object and sets the rental status to "Rented". The result is a new Mono<RentalResponse> containing this merged data.

To verify that the transformation works correctly, we use StepVerifier.create(rentalResponseMono). This subscribes to the rentalResponseMono and expects the emitted item to be a RentalResponse object with the values "CUST789" (customer ID), "MOV987" (movie ID), and "Rented" (status). The verifyComplete() method ensures that the Mono completes successfully without any errors.

3.4 Enhancing Code Reusability with transform() Method

The transform() method is useful when we need to apply a common transformation logic to multiple Mono objects.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
void testTransformValidation() {
    Function<Mono<Customer, Mono<Customer>> validateCustomer = mono
            -> mono.filter(Customer::isActive)
                    .switchIfEmpty(Mono.error(new RuntimeException("Customer is inactive")));
 
    Mono<Customer> activeCustomerMono = Mono.just(new Customer("CUST123", "Thomas", "thomas@jcg.com", true));
    Mono<Customer> inactiveCustomerMono = Mono.just(new Customer("CUST124", "Bob", "bob@jcg.com", false));
 
    // Active customer should pass validation
    StepVerifier.create(activeCustomerMono.transform(validateCustomer))
            .expectNextMatches(customer -> customer.getName().equals("Thomas"))
            .verifyComplete();
 
    // Inactive customer should fail validation
    StepVerifier.create(inactiveCustomerMono.transform(validateCustomer))
            .expectErrorMatches(error -> error instanceof RuntimeException && error.getMessage().equals("Customer is inactive"))
            .verify();
}

This block of code demonstrates how to use the transform() method in Spring WebFlux to apply reusable logic for validating Mono<Customer> objects. The purpose of this transformation is to filter out inactive customers while allowing active ones to pass through the reactive pipeline.

The transformation logic is defined using a Function<Mono<Customer>, Mono<Customer>> named validateCustomer. This function takes a Mono<Customer> as input and applies a filtering condition using .filter(Customer::isActive). If the Customer object is active (true), it remains in the pipeline. However, if the customer is inactive (false), switchIfEmpty() replaces the empty stream with an error signal, emitting a RuntimeException with the message "Customer is inactive". This ensures that inactive customers do not proceed further in the pipeline.

Two test cases are created using StepVerifier to verify this behavior. In the first case, an active customer named "Thomas" is wrapped in a Mono<Customer> and transformed using validateCustomer. Since Alice is active, the transformation allows her Customer object to pass through, and StepVerifier verifies that the emitted customer has the expected name "Thomas". The test confirms that the pipeline completes successfully using verifyComplete().

In the second case, an inactive customer named "Bob" is passed through the same validateCustomer transformation. Since Bob is inactive, the .filter() operation removes the item from the stream, triggering the switchIfEmpty() method, which throws an error instead of emitting a customer object. The StepVerifier in this test case expects an error and verifies that it matches the expected RuntimeException with the message "Customer is inactive".

3.5 Error Handling During Transformations

Spring WebFlux provides operators to handle errors gracefully and maintain the flow of reactive streams. One such operator is onErrorResume(), which allows us to recover from errors by providing an alternative Mono when an exception occurs. Instead of letting the reactive pipeline fail completely, onErrorResume() catches the error and replaces it with a fallback value or alternative processing logic.

The following example demonstrates how to handle errors when retrieving customer details and recovering with a default customer response in case of an exception.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
 void testErrorHandlingWithOnErrorResume() {
     Function<String, Mono<Customer>> findCustomerById = customerId -> {
         if ("CUST404".equals(customerId)) {
             return Mono.error(new RuntimeException("Customer not found"));
         }
         return Mono.just(new Customer(customerId, "Thomas", "alice@jcg.com", true));
     };
 
     Mono<Customer> validCustomerMono = findCustomerById.apply("CUST123")
             .onErrorResume(e -> Mono.just(new Customer("DEFAULT", "Guest", "guest@jcg.com", false)));
 
     Mono<Customer> missingCustomerMono = findCustomerById.apply("CUST404")
             .onErrorResume(e -> Mono.just(new Customer("DEFAULT", "Guest", "guest@jcg.com", false)));
 
     StepVerifier.create(validCustomerMono)
             .expectNextMatches(customer -> customer.getCustomerId().equals("CUST123"))
             .verifyComplete();
 
     StepVerifier.create(missingCustomerMono)
             .expectNextMatches(customer -> customer.getCustomerId().equals("DEFAULT") && customer.getName().equals("Guest"))
             .verifyComplete();
 }

In this example, we define a reusable function, findCustomerById, which takes a customerId and returns a Mono<Customer>. If the provided customerId is "CUST404", the function simulates a failure by returning Mono.error(new RuntimeException("Customer not found")). Otherwise, it returns a valid Customer object.

The key transformation occurs when we call onErrorResume(). This operator catches any errors that occur in the pipeline and provides a fallback value—in this case, a default guest customer with the ID "DEFAULT" and the name "Guest". This ensures that even if an error occurs, the application does not crash but instead returns a meaningful alternative.

To test this behavior, we create two test cases using StepVerifier. The first test case, validCustomerMono, retrieves an existing customer ("CUST123") and passes through the pipeline without any errors. The test verifies that the emitted customer has the expected customerId "CUST123".

The second test case, missingCustomerMono, attempts to retrieve a non-existent customer ("CUST404"), which triggers the error condition. Instead of failing, onErrorResume() provides a default guest user. The test verifies that the emitted customer has the ID "DEFAULT" and the name "Guest", ensuring that error handling works correctly.

4. Conclusion

In this article, we explored various techniques for transforming Mono objects in Spring WebFlux, demonstrating how different transformation methods facilitate efficient reactive data handling. Beginning with synchronous transformations using map(), we examined how data within a Mono can be modified without introducing asynchronous behavior. We then transitioned to asynchronous transformations using flatMap(), illustrating how it enables non-blocking data retrieval from external sources.

We further examined reusable transformation logic through the transform() method, showcasing how common operations such as validation can be modularized and applied consistently across multiple reactive streams. Additionally, we demonstrated how data merging from multiple Mono sources can be efficiently achieved using zipWith(), ensuring that distinct data entities can be combined into a unified result.

Finally, we addressed error handling during transformations, emphasizing the importance of resilience in reactive applications. By leveraging onErrorResume(), we illustrated how fallback strategies can be implemented to prevent pipeline failures and maintain application stability in the face of unexpected errors.

By applying these transformation techniques, we can construct highly responsive and efficient reactive systems that effectively manage both synchronous and asynchronous data flows.

5. Download the Source Code

This article explored how to convert a Mono object in Spring WebFlux.

Download
You can download the full source code of this example here: spring webflux convert mono object

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button