Spring Boot Topic
Spring Boot Asynchronous Processing and Reactive Programming
1. Asynchronous Processing in Spring Boot
What is Asynchronous Processing?
Asynchronous processing allows a program to handle multiple tasks concurrently without blocking the main thread. Instead of waiting for a task to complete before moving to the next one, the program can initiate a task and continue executing other operations, then handle the result when it becomes available.
Key Benefits:
-
Improved performance: Better utilization of system resources
-
Better scalability: Handle more concurrent requests
-
Responsive applications: Main thread remains responsive
Spring Boot Implementation:
Spring provides @Async annotation for simple asynchronous method execution.
java
@Service
public class NotificationService {
@Async // This method will be executed asynchronously
public CompletableFuture<String> sendNotification(String message) {
// Simulate time-consuming task
Thread.sleep(2000);
return CompletableFuture.completedFuture("Notification sent: " + message);
}
}
2. Reactive Programming with Project Reactor (Mono & Flux)
What is Reactive Programming?
Reactive programming is a programming paradigm focused on asynchronous data streams and the propagation of change. It's particularly useful for:
-
High-concurrency applications
-
Real-time systems
-
Applications with streaming data
Core Principles (Reactive Manifesto):
-
Responsive: Systems respond in a timely manner
-
Resilient: Systems stay responsive in the face of failure
-
Elastic: Systems stay responsive under varying workload
-
Message-driven: Rely on asynchronous message-passing
3. Mono and Flux Explained
Mono: Zero or One Element
Definition: A Mono is a reactive type that represents a stream which can emit 0 or 1 element, then complete. It's similar to a Future or Optional but with reactive capabilities.
Real-world Example: Fetching a single user profile from a database
-
Success case: Returns one user object
-
Error case: Returns error (no user)
-
Empty case: Returns empty (user not found)
Flux: Zero to N Elements
Definition: A Flux is a reactive type that represents a stream which can emit 0 to N elements, then complete. It's similar to a List or Collection but with reactive streaming capabilities.
Real-world Example: A live sports score feed
-
Multiple score updates over time
-
Continuous stream of data
-
Can complete when the game ends
4. Code Examples with Detailed Explanations
Example 1: Mono - Single User Retrieval
Scenario: Fetching a user from a database. We want to handle this reactively to avoid blocking threads.
java
import reactor.core.publisher.Mono;
@Service
public class UserService {
// Simulating a repository call
public Mono<User> findUserById(String userId) {
return Mono.fromCallable(() -> {
// Simulate database call
Thread.sleep(100); // Simulate latency
if ("valid-id".equals(userId)) {
return new User(userId, "John Doe", "john@example.com");
} else if ("empty-id".equals(userId)) {
return null; // Will be converted to Mono.empty()
} else {
throw new RuntimeException("Database error");
}
});
}
}
// Controller
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUser(@PathVariable String id) {
return userService.findUserById(id)
.map(user -> ResponseEntity.ok(user))
.defaultIfEmpty(ResponseEntity.notFound().build())
.onErrorResume(error ->
Mono.just(ResponseEntity.status(500).build())
);
}
}
Example 2: Flux - Real-time Stock Ticker
Scenario: Streaming real-time stock prices to clients. Each price update is an event in the stream.
java
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import java.time.Duration;
@Service
public class StockTickerService {
// Create a multicast sink for real-time updates
private final Sinks.Many<StockPrice> stockPriceSink =
Sinks.many().multicast().onBackpressureBuffer();
// Simulate stock price updates
public void startStockUpdates() {
Flux.interval(Duration.ofSeconds(1))
.map(tick -> generateRandomStockPrice())
.subscribe(stockPriceSink::tryEmitNext);
}
private StockPrice generateRandomStockPrice() {
String[] symbols = {"AAPL", "GOOGL", "MSFT", "AMZN"};
String symbol = symbols[(int)(Math.random() * symbols.length)];
double price = 100 + (Math.random() * 1000);
return new StockPrice(symbol, price);
}
// Stream for a specific stock symbol
public Flux<StockPrice> getStockStream(String symbol) {
return stockPriceSink.asFlux()
.filter(stock -> stock.getSymbol().equals(symbol))
.doOnSubscribe(sub ->
System.out.println("New subscriber for " + symbol)
);
}
}
// WebSocket Controller for real-time updates
@RestController
@RequestMapping("/stocks")
public class StockController {
@Autowired
private StockTickerService stockService;
@GetMapping(value = "/stream/{symbol}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<StockPrice> streamStockPrices(@PathVariable String symbol) {
return stockService.getStockStream(symbol)
.delayElements(Duration.ofSeconds(1)) // Throttle updates
.doOnNext(price ->
System.out.println("Sending: " + price)
);
}
}
Example 3: Combining Mono and Flux - E-commerce Order Processing
Scenario: Processing an order and its items reactively.
java
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class OrderService {
// Mono: Creating a single order
public Mono<Order> createOrder(OrderRequest request) {
return Mono.fromCallable(() -> {
// Validate and create order
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString());
order.setCustomerId(request.getCustomerId());
return order;
});
}
// Flux: Processing multiple order items
public Flux<OrderItem> processOrderItems(String orderId, List<ItemRequest> items) {
return Flux.fromIterable(items)
.flatMap(item -> processSingleItem(orderId, item))
.doOnNext(processedItem ->
System.out.println("Processed: " + processedItem)
)
.doOnComplete(() ->
System.out.println("All items processed for order: " + orderId)
);
}
// Mono: Processing single item
private Mono<OrderItem> processSingleItem(String orderId, ItemRequest item) {
return Mono.fromCallable(() -> {
// Simulate item processing
Thread.sleep(100); // Simulate processing time
OrderItem orderItem = new OrderItem();
orderItem.setItemId(UUID.randomUUID().toString());
orderItem.setProductId(item.getProductId());
orderItem.setQuantity(item.getQuantity());
orderItem.setOrderId(orderId);
return orderItem;
});
}
// Combining Mono and Flux
public Mono<OrderSummary> processCompleteOrder(OrderRequest request) {
return createOrder(request)
.flatMap(order -> {
// Process items after order is created
return processOrderItems(order.getOrderId(), request.getItems())
.collectList() // Convert Flux to Mono<List>
.map(items -> {
OrderSummary summary = new OrderSummary();
summary.setOrder(order);
summary.setItems(items);
summary.setTotalItems(items.size());
return summary;
});
});
}
}
5. Key Operators and Patterns
Common Mono Operators:
java
// Transforming
Mono<String> mono = Mono.just("hello");
Mono<Integer> lengthMono = mono.map(String::length);
// Handling errors
Mono<User> safeMono = getUser()
.onErrorReturn(fallbackUser)
.onErrorResume(error -> getBackupUser());
// Combining Monos
Mono<User> userMono = getUser();
Mono<Profile> profileMono = getProfile();
Mono<UserProfile> combined = userMono.zipWith(profileMono, UserProfile::new);
Common Flux Operators:
java
// Filtering and transforming
Flux<Integer> numbers = Flux.range(1, 10);
Flux<Integer> evenNumbers = numbers.filter(n -> n % 2 == 0);
// Buffering
Flux<List<Integer>> buffered = numbers.buffer(3);
// Windowing (similar to buffering but returns Flux of Flux)
Flux<Flux<Integer>> windowed = numbers.window(3);
// Backpressure handling
Flux<Integer> controlled = numbers.onBackpressureBuffer(100);
6. Best Practices
When to Use Asynchronous (@Async):
-
Simple background tasks
-
Email sending
-
File processing
-
When you don't need complex stream processing
When to Use Reactive (Mono/Flux):
-
High-concurrency APIs
-
Real-time streaming
-
Microservices communication
-
When you need backpressure management
-
Chat applications, live feeds, IoT data streams
Performance Considerations:
-
Cold vs Hot Publishers:
-
Cold: Starts emitting on subscription (database queries)
-
Hot: Emits regardless of subscribers (stock tickers)
-
-
Backpressure: Always consider how to handle when producer is faster than consumer
-
Resource Management: Use
doOnSubscribe,doOnCancel, anddoFinallyfor cleanup
7. Testing Reactive Code
java
@Test
public void testMonoUser() {
StepVerifier.create(userService.findUserById("valid-id"))
.expectNextMatches(user ->
user.getName().equals("John Doe")
)
.verifyComplete();
}
@Test
public void testFluxStockStream() {
StepVerifier.withVirtualTime(() ->
stockService.getStockStream("AAPL").take(3)
)
.thenAwait(Duration.ofSeconds(3))
.expectNextCount(3)
.verifyComplete();
}
Summary
Asynchronous Processing (@Async) is simpler and suitable for basic background tasks, while Reactive Programming (Mono/Flux) provides more powerful abstractions for handling streams of data with built-in backpressure and error handling.
Choose Mono when dealing with single or optional results (like API responses, single database records). Choose Flux when working with collections, streams, or multiple events over time (like WebSocket messages, file streams, database result sets).
The reactive approach shines in microservices architectures and real-time applications where non-blocking I/O and efficient resource utilization are critical.
Comments (0)
No comments yet. Be the first to comment!