How to Zip Three Different Monos of Different Types in Spring WebFlux: A Reactive Programming Guide

In the world of reactive programming, handling asynchronous data streams efficiently is key to building scalable and responsive applications. Spring WebFlux, built on Project Reactor, provides a powerful toolkit for writing reactive applications in Java. A common scenario in reactive programming is combining results from multiple asynchronous operations. For example, you might need to fetch data from three different services (e.g., a user profile, an order history, and a product catalog) and merge their results into a single response.

While zipping two Mono instances (which emit 0 or 1 item) is straightforward, zipping three Mono instances of different types requires careful handling of type safety and combinator functions. This guide will walk you through the process step-by-step, from understanding the basics of Mono and zip to implementing a practical example with error handling and advanced scenarios.

Table of Contents#

  1. Prerequisites
  2. Understanding Mono and zip in Spring WebFlux
  3. Why Zip Three Monos of Different Types?
  4. Step-by-Step Guide to Zipping Three Monos
  5. Handling Errors When Zipping Monos
  6. Advanced Scenarios
  7. Conclusion
  8. References

Prerequisites#

Before diving in, ensure you have the following:

  • Basic knowledge of Java (11+ recommended).
  • Familiarity with Spring Boot and Spring WebFlux.
  • Understanding of reactive programming concepts (e.g., Mono, Flux, backpressure).
  • A build tool (Maven or Gradle).
  • JDK 11 or higher.

Understanding Mono and zip in Spring WebFlux#

What is Mono?#

A Mono is a reactive type in Project Reactor that emits 0 or 1 item and then completes (successfully or with an error). It is ideal for representing asynchronous operations that return a single result (e.g., fetching a user by ID from a database).

What is zip?#

The zip operator combines multiple Mono (or Flux) instances into a single Mono (or Flux). It waits for all input streams to complete and then applies a combinator function to their results to produce a single output.

For Mono, Mono.zip() is used to combine results. It has several overloads, but the most useful for zipping three Mono instances is:

static <T1, T2, T3, O> Mono<O> zip(  
    Mono<? extends T1> source1,  
    Mono<? extends T2> source2,  
    Mono<? extends T3> source3,  
    Function3<? super T1, ? super T2, ? super T3, ? extends O> combinator  
)  

Here:

  • source1, source2, source3: The three Mono instances to zip (each can be of a different type).
  • combinator: A function that takes the results of the three Mono instances and returns a combined output (e.g., a DTO).

Why Zip Three Monos of Different Types?#

Zipping three Mono instances of different types is useful when:

  • You need to aggregate data from multiple independent sources (e.g., fetching a user, their order, and the product they ordered from separate microservices).
  • Each source returns a distinct data type (e.g., User, Order, Product), and you need to merge them into a unified response (e.g., UserOrderProductDTO).
  • You want to execute operations concurrently (since zip runs all Mono instances in parallel) and combine their results.

Step-by-Step Guide to Zipping Three Monos#

Let’s build a practical example where we zip three Mono instances of types User, Order, and Product into a combined DTO.

4.1 Project Setup#

First, create a new Spring Boot project with the "Spring WebFlux" dependency.

Using Maven#

Add this to your pom.xml:

<dependencies>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-webflux</artifactId>  
    </dependency>  
</dependencies>  

Using Gradle#

Add this to your build.gradle:

dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter-webflux'  
}  

4.2 Define Data Models#

Create three simple data classes to represent the types of data we’ll fetch: User, Order, and Product. We’ll use Java Records for conciseness (requires Java 16+; for older versions, use POJOs with getters).

// User.java  
public record User(Long id, String name, String email) {}  
 
// Order.java  
public record Order(Long id, Long userId, Double totalAmount) {}  
 
// Product.java  
public record Product(Long id, String name, Double price) {}  

Next, create a combined DTO to hold the merged results:

// UserOrderProductDTO.java  
public record UserOrderProductDTO(User user, Order order, Product product) {}  

4.3 Create Sample Monos#

Now, simulate three asynchronous operations returning Mono<User>, Mono<Order>, and Mono<Product>. For simplicity, we’ll use Mono.just() with dummy data, but in a real app, these might call external APIs or databases.

import reactor.core.publisher.Mono;  
 
@Service  
public class DataService {  
 
    // Simulate fetching a User (e.g., from a user service)  
    public Mono<User> fetchUser(Long userId) {  
        return Mono.just(new User(userId, "John Doe", "[email protected]"))  
                   .delayElement(Duration.ofMillis(100)); // Simulate network delay  
    }  
 
    // Simulate fetching an Order (e.g., from an order service)  
    public Mono<Order> fetchOrder(Long orderId) {  
        return Mono.just(new Order(orderId, 1L, 99.99)) // userId=1 (matches User.id=1)  
                   .delayElement(Duration.ofMillis(150));  
    }  
 
    // Simulate fetching a Product (e.g., from a product service)  
    public Mono<Product> fetchProduct(Long productId) {  
        return Mono.just(new Product(productId, "Wireless Headphones", 149.99))  
                   .delayElement(Duration.ofMillis(200));  
    }  
}  

4.4 Zip the Monos with a Combiner Function#

Use Mono.zip() to combine the three Mono instances. The combinator function will take the User, Order, and Product results and return a UserOrderProductDTO.

Create a service method to handle the zipping:

import reactor.core.publisher.Mono;  
 
@Service  
public class ZippingService {  
 
    private final DataService dataService;  
 
    public ZippingService(DataService dataService) {  
        this.dataService = dataService;  
    }  
 
    public Mono<UserOrderProductDTO> zipThreeMonos() {  
        // Fetch each Mono (simulated async calls)  
        Mono<User> userMono = dataService.fetchUser(1L);  
        Mono<Order> orderMono = dataService.fetchOrder(101L);  
        Mono<Product> productMono = dataService.fetchProduct(201L);  
 
        // Zip the three Monos and combine into UserOrderProductDTO  
        return Mono.zip(  
            userMono,  
            orderMono,  
            productMono,  
            (user, order, product) -> new UserOrderProductDTO(user, order, product)  
        );  
    }  
}  

Key Details:

  • Mono.zip(userMono, orderMono, productMono, ...) runs all three Mono instances concurrently (not sequentially).
  • The combinator function (user, order, product) -> new UserOrderProductDTO(...) takes the results of the three Mono instances and returns the combined DTO.

4.5 Expose as a REST Endpoint#

Expose the zipped result via a REST controller to test it:

import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.RestController;  
import reactor.core.publisher.Mono;  
 
@RestController  
public class ZippingController {  
 
    private final ZippingService zippingService;  
 
    public ZippingController(ZippingService zippingService) {  
        this.zippingService = zippingService;  
    }  
 
    @GetMapping("/combined-data")  
    public Mono<UserOrderProductDTO> getCombinedData() {  
        return zippingService.zipThreeMonos();  
    }  
}  

Testing the Endpoint#

Run the application and send a GET request to http://localhost:8080/combined-data. You’ll see a response like this:

{  
  "user": { "id": 1, "name": "John Doe", "email": "[email protected]" },  
  "order": { "id": 101, "userId": 1, "totalAmount": 99.99 },  
  "product": { "id": 201, "name": "Wireless Headphones", "price": 149.99 }  
}  

Handling Errors When Zipping Monos#

If any of the zipped Mono instances emit an error, the entire zipped Mono will fail immediately. To make the application robust, add error handling to individual Mono instances using operators like onErrorResume, onErrorReturn, or retry.

Example: Handle Errors in fetchProduct#

Suppose fetchProduct might fail (e.g., product not found). Let’s modify it to return a default Product on error:

// In DataService.java  
public Mono<Product> fetchProduct(Long productId) {  
    return Mono.just(new Product(productId, "Wireless Headphones", 149.99))  
               .delayElement(Duration.ofMillis(200))  
               // Simulate an error for productId=999  
               .flatMap(product -> productId == 999L ? Mono.error(new RuntimeException("Product not found")) : Mono.just(product))  
               // Return default product on error  
               .onErrorReturn(new Product(-1L, "Default Product", 0.0));  
}  

Now, if fetchProduct(999L) is called, it will return Product(-1, "Default Product", 0.0) instead of failing the entire zip operation.

Advanced Scenarios#

Zipping More Than Three Monos#

Mono.zip supports zipping more than three Mono instances using Mono.zip(mono1, mono2, mono3, mono4, combinator), where the combinator function accepts four arguments. For dynamic numbers of Mono instances, use Mono.zip(monos) (returns a Mono<TupleN>), but this is less type-safe.

Adding Timeouts#

To prevent long-running operations from blocking the zip, add timeouts to individual Mono instances with timeout(Duration):

Mono<User> userMono = dataService.fetchUser(1L)  
                                 .timeout(Duration.ofSeconds(2))  
                                 .onErrorResume(TimeoutException.class, e -> Mono.just(new User(-1L, "Timeout User", "[email protected]")));  

Sequential vs. Concurrent Execution#

By default, Mono.zip runs all Mono instances concurrently. To run them sequentially, chain them with flatMap, but this defeats the purpose of parallelism. Use zip for concurrency!

Conclusion#

Zipping three Mono instances of different types in Spring WebFlux is a powerful way to combine results from multiple asynchronous operations. By using Mono.zip() with a combinator function, you can safely merge distinct data types into a unified response. Remember to handle errors and timeouts to ensure robustness.

With this guide, you’re now equipped to handle complex reactive data aggregation scenarios in your Spring WebFlux applications.

References#