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.
You can download the full source code of this example here: spring webflux convert mono object