Advanced Tricks for Reactive Programming in Spring

ยท

3 min read

Why Choose Reactive Programming Over Traditional Programming?

Reactive programming is designed to handle asynchronous and event-driven architectures efficiently. Compared to traditional imperative programming, it provides significant advantages in terms of I/O handling and resource utilization:

  • Optimized Resource Utilization: Reactive applications use a non-blocking approach, avoiding thread-per-request models and maximizing CPU and memory efficiency.

  • Better Handling of I/O Operations: Traditional blocking calls (e.g., database queries, HTTP requests) tie up system resources, whereas reactive programming allows applications to process multiple I/O operations simultaneously without waiting for each to complete.

  • Scalability and Performance: Reactive systems can handle a higher number of concurrent users with fewer threads, making them ideal for microservices and cloud-based applications.

  • Event-Driven and Responsive: The ability to react dynamically to events (user input, external APIs, database changes) makes reactive programming a natural fit for modern applications.

In this article, we'll explore various real-world scenarios, explain the underlying operators, and how to translate them into reactive patterns effectively.


1. Conditional Execution (If-Else)

Understanding flatMap() and handle()

  • flatMap(): Transforms data into another reactive type (Mono/Flux), used when an operation returns another reactive stream.

  • handle(): A more flexible operator allowing conditionally emitting values.

Reactive Approach:

Using flatMap():

Mono<String> resultMono = Mono.just(input)
    .flatMap(data -> data.length() > 3 ? Mono.just("Processed: " + data) : Mono.just("Skipped"));

Using handle():

Mono.just(input)
    .handle((data, sink) -> {
        if (data.length() > 3) sink.next("Processed: " + data);
        else sink.next("Skipped");
    })
    .subscribe(System.out::println);

2. Looping (For/While)

Understanding Flux.fromIterable(), flatMap(), and concatMap()

  • Flux.fromIterable(): Converts an iterable (list, set) into a reactive stream.

  • flatMap(): Asynchronously processes elements in parallel.

  • concatMap(): Processes elements sequentially, preserving order.

Reactive Approach:

Using Flux.fromIterable():

Flux.fromIterable(Arrays.asList(1, 2, 3, 4, 5))
    .map(num -> num * 2)
    .subscribe(System.out::println);

For async processing:

Flux.range(1, 5)
    .flatMap(num -> Mono.just(num * 2).subscribeOn(Schedulers.parallel()))
    .subscribe(System.out::println);

Use concatMap() instead of flatMap() if ordering is required.


3. Error Handling (Try-Catch)

Understanding onErrorResume() and onErrorMap()

  • onErrorResume(): Provides an alternative value or stream when an error occurs.

  • onErrorMap(): Transforms an exception into a different type.

Reactive Approach:

Using onErrorResume():

Mono.just("data")
    .flatMap(data -> riskyMethodReactive(data))
    .onErrorResume(e -> Mono.just("Fallback Data"))
    .subscribe(System.out::println);

Using onErrorMap():

Mono.just("data")
    .flatMap(data -> riskyMethodReactive(data))
    .onErrorMap(e -> new CustomException("Custom error: " + e.getMessage()))
    .subscribe(System.out::println);

4. Combining Multiple API Calls (Parallel Processing)

Understanding zip() and merge()

  • zip(): Combines multiple Monos/Flux by waiting for all sources to emit and then combining them.

  • merge(): Combines multiple Flux without waiting for all to complete.

Reactive Approach:

Using zip():

Mono.zip(service1Reactive(), service2Reactive(), (data1, data2) -> data1 + " & " + data2)
    .subscribe(System.out::println);

Using merge():

Flux.merge(service1Reactive(), service2Reactive())
    .subscribe(System.out::println);

5. Delaying Execution

Understanding delayElement() and interval()

  • delayElement(): Delays emission of a Mono/Flux by a specified duration.

  • interval(): Emits a sequence of numbers at a fixed interval.

Reactive Approach:

Using delayElement():

Mono.just("Done")
    .delayElement(Duration.ofSeconds(2))
    .subscribe(System.out::println);

Using interval():

Flux.interval(Duration.ofSeconds(1))
    .map(i -> "Tick " + i)
    .take(5)
    .subscribe(System.out::println);

Final Thoughts

  • Use filter() for simple conditions, handle() for complex logic.

  • Replace loops with Flux and use flatMap() for async processing.

  • Use onErrorResume() or onErrorMap() for error handling.

  • Use zip() for dependent API calls or merge() for parallel calls.

  • Replace Thread.sleep() with delayElement().

  • Use retry() for retry logic with retryWhen() for backoff strategies.

  • Cache expensive operations using cache().

  • Manage backpressure using onBackpressureDrop() or buffer().

By following these patterns, you can fully utilize the power of reactive programming in Spring ๐Ÿš€

ย