Explore reactive programming before diving into implementation:
Introduction:
Modern applications demand high responsiveness, efficient resource usage, and scalability. Traditional Java applications based on thread-per-request models often struggle to meet these expectations, especially under high concurrency and latency-sensitive conditions.
This is where reactive programming becomes a powerful solution. But before implementing reactive APIs using Reactor or RxJava, it is important to understand why and when this approach is necessary and what problems it aims to solve.
This article is for mid-level and experienced software professionals who already have some hands-on experience in building applications and want to understand when and why to use reactive programming. It focuses on the reasons for using this approach before getting into how to use it in code.
When to Use Reactive Programming:
Reactive programming is suitable for:
- High-concurrency applications
- IO-bound services (APIs, databases, messaging)
- Microservices communicating over non-blocking interfaces
- Streaming data processing and real-time analytics
It is not ideal for:
- CPU-intensive computations
- Simple CRUD applications with limited traffic
- Projects where team experience with reactive frameworks is limited
Why Choose Reactive Programming in Java
Uses System Resources Wisely: In normal Java programs, a thread will just sit and wait during tasks like calling an API or reading from a database. But reactive programming lets the thread go do other work while waiting. This helps your system run better without needing extra hardware.
Handles More Users Without Extra Load: Reactive apps can support many users at the same time without creating more threads. This means the app uses less memory and still works fast.
Easy to Manage Errors and Data Flow: Reactive programming handles regular data, errors, and finished tasks in the same way. This makes it easy to deal with problems and control the flow of data when there is too much coming in.
Keeps Everything Moving Smoothly: Reactive apps do not pause or block anything. They keep running tasks in the background. This is very useful when building fast and responsive apps like chat systems, live updates, or cloud-based services.
Required Dependencies for Reactive Programming in Java:
Reactive programming in Java depends on several libraries. Below is a short list of important ones:
<!-- Reactor core (main library for Mono and Flux) -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<!-- Reactor Netty (used under WebFlux for non-blocking HTTP) -->
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
<!-- Spring WebFlux (for building reactive REST APIs) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Optional: for reactive database access -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- Optional: R2DBC driver (example for PostgreSQL) -->
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
<!-- Optional: MongoDB reactive support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
To keep things simple and clear, I am going to use Spring WebFlux to show how reactive programming works in Java. WebFlux is easy to start with and supports all the key concepts like Mono, Flux, non-blocking APIs, and streaming.
You can also explore other libraries if you want to try different styles later.
Code Examples:
Before We Dive Into Code, Let’s Understand the Core Concepts: To follow the upcoming examples with clarity, here’s a quick overview of the key reactive programming terms used in Java (especially with Spring WebFlux):
Mono: Represents a stream that emits zero or one value. Ideal for handling single responses such as fetching a user by ID.
Flux: Represents a stream that emits zero or more values. Useful for handling lists, real-time data streams, or multiple events.
flatMap: Used to asynchronously compose or chain reactive streams. Allows you to call one reactive method after another, without blocking.
onErrorResume: Provides a graceful fallback mechanism when an error occurs in a reactive flow. Helps maintain stability without breaking the stream.
zip: Combines multiple reactive sources and emits a combined result when all inputs are available. Great for merging data from different services.
parallel: Enables multi-threaded execution of stream elements, improving performance for CPU-bound tasks without manually managing threads.
backpressure: A strategy to handle situations where the data producer is faster than the consumer. Reactor handles this elegantly using operators like onBackpressureBuffer.
WebClient: A non-blocking HTTP client provided by Spring WebFlux to make reactive REST calls. It replaces RestTemplate in reactive applications.
StepVerifier: A powerful testing tool from Reactor to validate Mono and Flux behavior step by step in unit tests.
1. Returning a single valueString vs Mono :
// Normal style(Blocking)
public String greet(String name) {
return "Hello " + name;
}
// Reactive style(Non-Blocking)
public Mono<String> greet(String name) {
return Mono.just("Hello " + name);
}
Normal: The method executes and returns the result immediately. The thread is held until completion.
Reactive: Mono is a wrapper that will emit one value in the future. You do not block the thread; the actual work can be chained and subscribed later.
Why it matters: Use reactive when the response might be delayed, fetched remotely, or need further composition.
2. Returning a list List vs Flux :
// Normal style(Blocking)
public List<String> findAllNames() {
return List.of("Arun", "Bhavya", "Chitra");
}
// Reactive style(Non-Blocking)
public Flux<String> findAllNames() {
return Flux.just("Arun", "Bhavya", "Chitra");
}
Normal: All names must be fetched before returning. The thread waits until the full list is ready.
Reactive: Flux emits names one by one, allowing the client to start processing earlier and handle streaming data (like WebSockets or SSE).
Why it matters: Better for real-time streaming or paginated APIs.
3. Async chaining Manual calls vs flatMap :
// Normal style(Blocking)
public UserProfile buildProfile(String id) {
User user = userRepo.findById(id); // blocking
Address address = addressService.get(user.getPinCode()); // blocking
return new UserProfile(user.getName(), address.getCity());
}
// Reactive style(Non-Blocking)
public Mono<UserProfile> buildProfile(String id) {
return userRepo.findById(id)
.flatMap(user -> addressService.get(user.getPinCode())
.map(address -> new UserProfile(user.getName(), address.getCity())));
}
Normal: Each step waits until the previous completes. Slows down your server and wastes resources.
Reactive: flatMap chains async calls. The system continues only when data is available, without holding threads.
Why it matters: Excellent for integrating multiple APIs or database calls efficiently.
4. Error handling try-catch vs onErrorResume :
// Normal style(Blocking)
try {
User user = userRepo.findById("1");
} catch (Exception e) {
user = fallbackUser();
}
// Reactive style(Non-Blocking)
userRepo.findById("1")
.onErrorResume(e -> Mono.just(fallbackUser()))
.subscribe(System.out::println);
Normal: Errors are caught using try-catch, which often breaks flow and adds verbosity.
Reactive: Errors flow like data. You handle them inline using onErrorResume.
Why it matters: Helps in building clean, fault-tolerant pipelines.
5. Combining data Manual vs zip :
// Normal style(Blocking)
User user = userRepo.find("1");
Order order = orderRepo.findByUserId("1");
return new Summary(user, order);
// Reactive style(Non-Blocking)
Mono<Summary> summary = Mono.zip(
userRepo.find("1"),
orderRepo.findByUserId("1")
).map(tuple -> new Summary(tuple.getT1(), tuple.getT2()));
Normal: Both calls must finish before returning. They’re executed in sequence or manually in parallel.
Reactive: Mono.zip runs both in parallel and waits to combine results when both are ready.
Why it matters: Great for merging results from independent services efficiently.
6. Streaming List vs Flux with delay:
// Normal style(Blocking)
public List<Integer> getNumbers() {
return List.of(1, 2, 3, 4, 5); // All at once
}
// Reactive style(Non-Blocking)
public Flux<Integer> getNumbers() {
return Flux.range(1, 5)
.delayElements(Duration.ofSeconds(1));
}
Normal: All values are returned together. No control over pacing or memory usage.
Reactive: You emit one value every second, perfect for streaming responses like chat, logs, or prices.
Why it matters: Use in endpoints with Server-Sent Events or WebSockets.
7. Controlling flow Queue vs Backpressure:
// Normal style(Blocking)
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < 1000; i++) {
queue.add(i); // Overflows if slow
}
// Reactive style(Non-Blocking)
Flux.range(1, 1000)
.onBackpressureBuffer(100)
.subscribe(System.out::println);
Normal: If consumer is slow, the queue fills up and crashes.
Reactive: Backpressure buffers or drops values when the consumer can’t keep up.
Why it matters: Prevents memory leaks and performance crashes.
8. Making HTTP calls RestTemplate vs WebClient :
// Normal style(Blocking)
RestTemplate rest = new RestTemplate();
String response = rest.getForObject(url, String.class);
// Reactive style(Non-Blocking)
WebClient webClient = WebClient.create();
webClient.get()
.uri("https://api.example.com/data")
.retrieve()
.bodyToMono(String.class)
.subscribe(System.out::println);
Normal: The thread waits until the HTTP call completes.
Reactive: No thread is blocked. WebClient waits for the response asynchronously.
Why it matters: Useful in microservices where many APIs interact concurrently.
9. Testing assertEquals vs StepVerifier :
// Normal style(Blocking)
String result = service.greet("Madhan");
assertEquals("Hello Madhan", result);
// Reactive style(Non-Blocking)
StepVerifier.create(service.greet("Madhan"))
.expectNext("Hello Madhan")
.verifyComplete();
Normal: You check return values directly.
Reactive: You verify stream behavior step by step.
Why it matters: Crucial for reliable testing of Mono and Flux .
10. Multiply numbers from 1 to 8 by 10 using parallel processing:
// Normal style(Blocking)
public void processInParallelBlocking() {
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 1; i <= 8; i++) {
int number = i;
executor.submit(() -> {
int result = number * 10;
System.out.println("Result: " + result + " | Thread: " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
// Reactive style(Non-Blocking)
public void processInParallelReactive() {
Flux.range(1, 8)
.parallel(4) // split into 4 parallel rails
.runOn(Schedulers.parallel()) // use Reactor's thread pool
.map(n -> {
int result = n * 10;
System.out.println("Result: " + result + " | Thread: " + Thread.currentThread().getName());
return result;
})
.sequential()
.subscribe();
}
Normal: You manually create threads using ExecutorService. You manage task submission, thread shutdown, and error handling on your own.
Reactive: You use parallel() with runOn() to split and process the data using Reactor's internal thread pool. No manual thread handling. The flow is fully asynchronous and efficient.
Why it matters: Great for speeding up CPU-heavy tasks without blocking threads. Clean, scalable, and production-ready.
Conclusion:
Understanding the purpose of reactive programming helps teams adopt it with clear goals. It is not meant to completely replace traditional synchronous programming, but it offers a smart way to build systems that are scalable, efficient, and responsive.
Before learning about the specific APIs and operators, it is important to understand the core ideas behind reactive programming such as non blocking execution, clear error handling, and event driven data flow. Once this foundation is clear, building and managing reactive systems becomes easier and more effective.
Top comments (0)