Spring WebFlux and Project Reactor

18 Nov 2025 . Tech . Comments
#SpringBoot #Microservices #Java #SoftwareEngineering #Architecture #WebFlux #Stream

A Hands-On Guide to Spring WebFlux and Project Reactor

Learn reactive programming through a practical, interactive demo application


Introduction

Reactive programming has become a cornerstone of modern Java development, especially when building high-performance, scalable applications. But learning reactive concepts can be challenging, as the abstract nature of streams, backpressure, and non-blocking I/O often requires hands-on experience to truly understand.

That’s why I built the Reactive Stream Demo, a standalone Spring Boot application that demonstrates 10 different reactive programming patterns through interactive examples. In this blog post, I’ll walk you through what makes this project special, how it works, and how you can use it to master reactive programming.

But first let’s define the basics.

What is Reactive Programming?

Before diving into the project, let’s quickly recap what reactive programming is all about:

Reactive programming is a programming paradigm focused on asynchronous data streams and the propagation of change. Instead of blocking threads waiting for data, reactive applications handle streams of data asynchronously, enabling:

  • Non-blocking I/O: Handle thousands of concurrent connections with minimal threads
  • Backpressure handling: Automatically manage fast producers and slow consumers
  • Composability: Build complex data processing pipelines from simple operations
  • Resilience: Built-in error handling and recovery mechanisms

The Reactive Streams specification (implemented by Project Reactor in Spring) provides a standard for asynchronous stream processing with non-blocking backpressure.

About Reactive Stream Demo

The Reactive Stream Demo is a comprehensive Spring Boot application that showcases reactive programming patterns through 10 interactive scenarios. It’s built with:

  • Java 21 - Latest LTS version with modern language features
  • Spring Boot 3.2.0 - Latest Spring Boot with full reactive support
  • Spring WebFlux - Reactive web framework
  • Project Reactor - Reactive streams implementation
  • Server-Sent Events (SSE) - Real-time client communication

Key Features

  1. 10 Interactive Demo Scenarios - From basic operations to advanced patterns
  2. Real-time Web Interface - Visualize streams as they happen
  3. Performance Metrics - Monitor throughput, latency, and resource usage

The 10 Demo Scenarios

Let me walk you through each scenario and what it teaches:

1. Basic Operations

What it demonstrates: Fundamental reactive stream operations

@GetMapping(value = "/basic-operations", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> basicOperations() {
    return Flux.range(1, 20)
            .map(i -> "Number: " + i)
            .filter(s -> s.contains("1") || s.contains("5"))
            .map(s -> s + " (filtered)")
            .delayElements(Duration.ofMillis(500));
}

This demo shows the bread and butter of reactive programming:

  • Flux.range(): Creates a stream of numbers
  • map(): Transforms each element
  • filter(): Selects elements based on conditions
  • delayElements(): Introduces time-based behavior

Key Learning: Reactive streams are composable and you chain operations to build data pipelines.

2. Backpressure Handling

What it demonstrates: Managing fast producers and slow consumers

@GetMapping(value = "/backpressure", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> backpressureDemo() {
    return Flux.range(1, 1000)
            .map(i -> "Fast data: " + i)
            .onBackpressureBuffer(10) // Buffer up to 10 items
            .delayElements(Duration.ofMillis(100));
}

Backpressure occurs when a producer generates data faster than the consumer can process it. This scenario shows:

  • onBackpressureBuffer(): Buffers excess items
  • onBackpressureDrop(): Drops items when buffer is full
  • Automatic flow control: Reactor handles backpressure signals automatically

Key Learning: Reactive streams automatically signal when they can’t keep up, preventing memory issues.

3. Error Handling and Recovery

What it demonstrates: Resilient error handling patterns

@GetMapping(value = "/error-handling", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> errorHandlingDemo() {
    return Flux.range(1, 10)
            .map(i -> {
                if (i == 5) {
                    throw new RuntimeException("Simulated error at item 5");
                }
                return "Item: " + i;
            })
            .onErrorResume(error -> {
                log.warn("Error occurred: {}", error.getMessage());
                return Flux.just("Error recovered: " + error.getMessage());
            })
            .retry(2);
}

This demo showcases:

  • onErrorResume(): Provides fallback data when errors occur
  • retry(): Automatically retries failed operations
  • Error propagation: How errors flow through the reactive chain

Key Learning: Reactive streams provide powerful error handling that keeps your application resilient.

4. Hot vs Cold Streams

What it demonstrates: Stream behavior differences

@GetMapping(value = "/hot-cold-streams", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> hotColdStreamsDemo() {
    Flux<String> coldStream = Flux.range(1, 5)
            .map(i -> "Cold: " + i)
            .delayElements(Duration.ofMillis(200));
    
    return coldStream
            .share() // This line converts cold to hot stream
            .doOnNext(event -> log.debug("Hot/Cold: {}", event));
}
  • Cold streams: Each subscriber gets a fresh, complete sequence
  • Hot streams: All subscribers share the same data stream

Key Learning: Understanding hot vs cold streams is crucial for building efficient reactive applications.

5. Combining Streams

What it demonstrates: Stream composition patterns

@GetMapping(value = "/combining-streams", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> combiningStreamsDemo() {
    Flux<String> stream1 = Flux.range(1, 5)
            .map(i -> "Stream1: " + i)
            .delayElements(Duration.ofMillis(300));
    
    Flux<String> stream2 = Flux.range(10, 5)
            .map(i -> "Stream2: " + i)
            .delayElements(Duration.ofMillis(400));
    
    return Flux.merge(stream1, stream2);
}

This scenario presents:

  • merge(): Combines multiple streams into one
  • zip(): Pairs elements from different streams
  • combineLatest(): Combines the latest values from multiple streams

Key Learning: Reactive programming excels at combining multiple data sources.

6-10. Additional Scenarios

The remaining scenarios cover:

  • Time Operations: Intervals, timeouts, and scheduling
  • Buffer & Window: Batching and windowing operations
  • Real-time Events: Live event generation and streaming
  • Performance Metrics: System monitoring and analytics
  • Interactive Control: Dynamic stream management via API

Architecture and Design

Technology Stack

The project uses a modern, production-ready stack:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-core</artifactId>
    </dependency>
</dependencies>

Key Design Decisions

  1. Server-Sent Events (SSE): Used for real-time streaming to the web interface
    • Simple protocol, works over HTTP
    • Automatic reconnection support
    • Perfect for one-way data streaming -
  2. Comprehensive Monitoring: Built-in observability
    • Spring Actuator for health checks
    • Micrometer for metrics
    • Prometheus endpoint for monitoring

Real-World Applications

The patterns demonstrated in this project are used in real-world applications:

1. High-Throughput APIs

Reactive programming shines when building APIs that need to handle thousands of concurrent requests. The non-blocking nature means you can serve more requests with fewer resources.

Example: A microservice that processes thousands of events per second from a message queue.

2. Real-time Data Processing

Streaming data processing is a natural fit for reactive programming. The backpressure handling ensures your application doesn’t get overwhelmed.

Example: Processing real-time analytics from user interactions, IoT sensors, or financial market data.

3. Microservices Communication

When microservices communicate asynchronously, reactive streams provide a clean abstraction.

Example: A service that aggregates data from multiple downstream services and streams the results.

4. WebSocket and SSE Applications

Real-time web applications benefit from reactive programming’s streaming capabilities.

Example: Live dashboards, chat applications, or collaborative editing tools.

Performance Benefits

Reactive programming isn’t just about code style—it delivers real performance benefits:

Traditional vs Reactive

Traditional (Blocking):

@GetMapping("/data")
public List<Data> getData() {
    return database.findAll(); // Blocks thread
}
  • One thread per request
  • Threads blocked waiting for I/O
  • Limited concurrency (typically 200-500 threads)

Reactive (Non-blocking):

@GetMapping(value = "/data", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Data> getData() {
    return database.findAllReactive(); // Non-blocking
}
  • Event loop handles thousands of connections
  • No threads blocked on I/O
  • Much higher concurrency (10,000+ connections)

Measured Improvements

In my testing, the reactive approach showed:

  • 10x higher throughput for I/O-bound operations
  • Lower memory usage due to efficient backpressure handling
  • Better resource utilization with fewer threads

Learning Path

If you’re new to reactive programming, here’s a suggested learning path using this project:

  1. Start with Basic Operations - Understand map, filter, and transform
  2. Explore Error Handling - Learn how errors flow through streams
  3. Study Backpressure - Understand flow control mechanisms
  4. Experiment with Combining Streams - See how to compose complex operations
  5. Build Your Own Demo - Propose your scenario to be added in the project

Code Examples

Let me show you a more complex example from the project:

Real-time Event Generation

public Flux<StreamEvent> generateRealTimeEvents() {
    return Flux.interval(Duration.ofMillis(500))
            .map(tick -> {
                String[] eventTypes = {
                    "order-created", 
                    "payment-processed", 
                    "inventory-updated"
                };
                String[] sources = {
                    "order-service", 
                    "payment-service", 
                    "inventory-service"
                };
                
                String eventType = eventTypes[(int) (tick % eventTypes.length)];
                String source = sources[(int) (tick % sources.length)];
                
                return StreamEvent.of(
                    eventType,
                    source,
                    Map.of(
                        "eventId", eventCounter.incrementAndGet(),
                        "timestamp", LocalDateTime.now(),
                        "data", "Demo event data " + tick
                    )
                );
            })
            .take(50)
            .doOnNext(event -> log.debug("Generated event: {}", event));
}

This example demonstrates:

  • Flux.interval(): Creates a time-based stream
  • Stateful transformations: Using external counters
  • Stream termination: take(50) limits the stream
  • Side effects: doOnNext() for logging

Best Practices Demonstrated

The project follows several best practices:

1. Proper Error Handling

.onErrorResume(error -> {
    log.warn("Error occurred: {}", error.getMessage());
    return Flux.just(StreamEvent.of(
        "error-recovery",
        "demo-service",
        Map.of("error", error.getMessage())
    ));
})
.retry(2)

2. Resource Management

public Mono<String> stopStream(String streamId) {
    Sinks.Many<StreamEvent> streamSink = activeStreams.remove(streamId);
    if (streamSink != null) {
        streamSink.tryEmitComplete();
        log.info("Stopped stream: {}", streamId);
        return Mono.just("stopped");
    }
    return Mono.just("not-found");
}

What Makes This Project Special

  1. Educational Focus: Each demo is designed to teach a specific concept
  2. Interactive: Real-time web interface makes learning engaging
  3. Production-Ready: Includes monitoring, health checks, and proper error handling

Conclusion

The Reactive Stream Demo project demonstrates that reactive programming doesn’t have to be abstract or difficult to understand. Through hands-on examples and a real-time interface, you can see reactive patterns in action.

Whether you’re:

  • Learning reactive programming for the first time
  • Preparing for technical interviews that focus on modern Java
  • Building high-performance applications that need to scale
  • Creating educational content about reactive programming

This project provides a solid foundation.

Try it yourself: Live Demo


Resources


Happy reactive programming! 🚀

If you found this post helpful, consider starring the root of this blogpost or sharing it with others learning reactive programming.


Me

Marios Gavriil is an awesome person. He lives between Jigglypuffs and Deadpools figures and loves unicorns. In his spare time composing music or experimenting new recipes in the kitchen