Reactive Notes
Reactive Notes
The concept is defined by the Reactive Manifesto, which describes the key
characteristics of reactive applications.
[Link]
Definition
A Reactive System is a system that:
Key Points
Users should receive quick responses to requests.
Example
An online banking application should display account balances within
seconds, even during peak usage hours.
2. Resilient
A reactive system should remain operational even when failures occur.
Key Points
Failures should be isolated.
Example
If a payment service fails, the order service should continue
functioning and retry the payment later.
3. Elastic
A reactive system should scale according to demand.
Key Points
Resources can be added automatically during high traffic.
Example
During a festival sale, an e-commerce application automatically
increases server instances to handle additional users.
4. Message-Driven
Reactive systems communicate using asynchronous messages.
Key Points
Components are loosely coupled.
Example
Order Service sends a message to Inventory Service through a
message broker like Apache Kafka without waiting for an immediate
response.
Benefits of Reactive Systems
Improved User Experience
Faster responses.
Better availability.
Reduced downtime.
Better Scalability
Can handle millions of requests.
Enhanced Reliability
Failure isolation.
High availability.
Characteristics
No waiting for I/O operations.
Better throughput.
2. Non-Blocking Execution
Threads are not blocked while waiting for results.
3. Event-Driven Architecture
Applications react to events.
Examples of Events
User clicks a button.
Payment completed.
4. Data Streams
Reactive programming treats data as streams. Instead of processing one
value at a time, the application continuously reacts to incoming values.
5. Backpressure
Backpressure prevents consumers from being overwhelmed by data
producers.
Problem
Producer -> 10000 events/sec
Consumer -> 100 events/sec
Solution
Consumer requests data at its own pace.
Spring MVC
Java Servlets
JAX-RS
How It Works
When a request arrives:
4. The thread waits for database calls, API calls, file operations, etc.
5. After sending the response, the thread is released back to the pool.
Flow
Request 1 --> Thread 1
Request 2 --> Thread 2
Request 3 --> Thread 3
Request 4 --> Thread 4
Spring WebFlux
Project Reactor
Netty
[Link]
Vert.x
The Event Loop Model uses a small number of threads to handle thousands
of requests.
How It Works
Instead of assigning one thread per request:
1. Request arrives.
o Database responses
o File operations
o Network communication
Examples include:
o Chat applications
o Live notifications
5. To Improve Scalability
Applications must scale as user traffic increases.
Reactive applications can handle more requests with fewer hardware
resources.
9. To Support Backpressure
Producers may generate data faster than consumers can process it.
They support:
o Error handling
o Retry mechanisms
o Timeout management
o Fallback strategies
Backpressure support.
High throughput.
Event-driven architecture.
Asynchronous processing.
1. Publisher
2. Subscriber
3. Subscription
4. Processor
1. Publisher
Definition
A Publisher is a data producer that generates and emits data items to one
or more subscribers.
It is the starting point of a reactive stream.
Responsibilities of Publisher
Produces data items.
Handles backpressure.
Key Characteristics
Can have multiple subscribers.
Examples
Reactor
Flux<Integer> numbers =
[Link](1, 10);
Real-World Analogy
Consider a newspaper company:
Newspapers = Data
Subscribers = Readers
void onComplete();
}
Responsibilities of Subscriber
Subscribes to a publisher.
Requests data.
Handles errors.
Purpose:
2. onNext()
Called whenever a new item arrives.
Example:
public void onNext(Integer value) {
[Link](value);
}
3. onError()
Called when an error occurs.
Example:
public void onError(Throwable error) {
[Link]([Link]());
}
4. onComplete()
Called when all data has been successfully delivered.
Real-World Analogy
Food Delivery System:
Restaurant = Publisher
Customer = Subscriber
3. Subscription
Definition
A Subscription represents the connection between a Publisher and a
Subscriber.
It is responsible for demand management and cancellation.
void cancel();
}
Responsibilities of Subscription
Establishes communication channel.
Supports backpressure.
request(long n)
Requests a specific number of items from publisher.
[Link](10);
cancel()
Terminates subscription.
[Link]();
With Subscription:
Real-World Analogy
Online Shopping:
Customer can:
o Cancel order.
4. Processor
Definition
A Processor acts as both:
Subscriber
Publisher
Responsibilities of Processor
Receives data from upstream publisher.
Data Flow
Publisher
|
v
Processor
|
v
Subscriber
Interaction Between Components
Step-by-Step Flow
Step 1: Subscriber subscribes to Publisher.
[Link](subscriber);
[Link](error);
1. reactor-core
reactor-core is the primary module of Project Reactor.
Purpose
Key Features
Reactive Streams compliant.
Backpressure support.
Functional programming style APIs.
Common Operators
Creation Operators
o just()
o fromIterable()
o empty()
o error()
o range()
Transformation Operators
o map()
o flatMap()
o concatMap()
Filtering Operators
o filter()
o take()
o skip()
Combination Operators
o merge()
o concat()
o zip()
2. reactor-test
reactor-test provides utilities for testing reactive applications.
Purpose
Testing Flux and Mono sequences.
Key Components
StepVerifier
Most commonly used testing utility.
Used for:
Verifying completion.
Verifying errors.
3. reactor-netty
reactor-netty is a reactive networking library built on top of the Netty
framework.
Purpose
Provides non-blocking HTTP/TCP/UDP clients and servers.
Key Features
Event Loop Architecture.
Non-blocking I/O.
High scalability.
HTTP/1.1 support.
HTTP/2 support.
WebSocket support.
TCP/UDP communication.
Reactor Core Types
Project Reactor provides two primary reactive types in the reactor-core
module:
1. Mono<T>
2. Flux<T>
These types implement the Reactive Streams Publisher interface and form
the foundation of reactive programming in Project Reactor and Spring
WebFlux.
1. Mono<T>
Definition
Mono<T> is a Publisher that emits:
o An error signal
Characteristics
Represents an asynchronous computation producing a single result.
Similar to:
Mono
│
└── onError(exception)
2. Flux<T>
Definition
Flux<T> is a Publisher that emits:
o Zero items
o One item
o Multiple items
o Infinite items
o Error signal
Characteristics
Represents a stream of data.
Processing events.
Chat applications.
[Link]()
Purpose
Creates a Mono that emits a specified value.
Syntax
Mono<String> mono = [Link]("Spring");
Characteristics
Immediately emits the value.
Example
[Link]("Java")
.subscribe([Link]::println);
[Link]()
Purpose
Creates a Mono from a nullable value.
Example
String name = null;
Benefits
Eliminates null checks.
[Link]()
Purpose
Creates a Mono that emits no value.
Example
Mono<String> mono = [Link]();
Signal Flow
onComplete()
Use Cases
No data found.
Optional results.
Empty responses.
[Link]()
Purpose
Creates a Mono that immediately emits an error.
Example
Mono<String> mono =
[Link](
new RuntimeException("Error"));
Signal Flow
onError()
Use Cases
Validation failure.
Business exceptions.
[Link]()
Purpose
Creates a Mono from a Callable.
Example
Mono<String> mono =
[Link](() ->
"Generated Value");
Benefits
Deferred execution.
[Link]()
Purpose
Creates Mono from a Supplier.
Example
Mono<String> mono =
[Link](() ->
"Dynamic Value");
Characteristics
Executes only when subscribed.
[Link]()
Purpose
Delays Mono creation until subscription time.
Example
Mono<String> mono =
[Link](() ->
[Link](getData()));
Benefits
Fresh data for each subscription.
Lazy initialization.
[Link]()
Purpose
Delays Mono creation until subscription time.
Example
Mono<String> mono =
[Link](() ->
[Link](getData()));
Benefits
Fresh data for each subscription.
Lazy initialization.
map()
Purpose
Converts one value into another value.
Synchronous transformation.
Example
[Link]("java")
.map(String::toUpperCase);
Characteristics
Returns Mono<R>.
Executes synchronously.
flatMap()
Purpose
Converts value into another Mono.
Example
[Link]("java")
.flatMap(value ->
[Link]([Link]()));
Characteristics
Used when transformation returns Mono.
cast()
Purpose
Converts object type.
Example
Mono<Object> mono =
[Link]("Spring");
[Link]([Link]);
3. Filtering Methods
Used to decide whether a value should continue.
filter()
Purpose
Passes value only if condition is true.
Example
[Link]("Spring")
.filter(name ->
[Link]() > 5);
filterWhen()
Purpose
Performs asynchronous filtering.
Example
[Link]("Spring")
.filterWhen(
value ->
[Link]([Link]() > 5));
Benefits
Reactive condition evaluation.
Database-based validation.
defaultIfEmpty()
Purpose
Provides default value.
Example
[Link]()
.defaultIfEmpty("Default");
switchIfEmpty()
Purpose
Switches to another Mono.
Example
[Link]()
.switchIfEmpty(
[Link]("Fallback"));
Use Cases
Cache fallback.
Database fallback.
onErrorReturn()
Purpose
Returns fallback value.
Example
[Link](new RuntimeException())
.onErrorReturn("Default");
onErrorResume()
Purpose
Switches to another Mono when error occurs.
Example
[Link](new RuntimeException())
.onErrorResume(
ex -> [Link]("Recovered"));
Use Cases
Service fallback.
Cache lookup.
onErrorMap()
Purpose
Converts one exception into another.
Example
[Link](new RuntimeException())
.onErrorMap(
ex ->
new IllegalArgumentException());
doOnError()
Purpose
Executes side-effect logic.
Example
[Link](new RuntimeException())
.doOnError(
error ->
[Link]([Link]()));
Use Cases
Logging.
Monitoring.
Metrics collection.
6. Side Effect Methods
Used for logging and monitoring.
doOnNext()
Purpose
Executes action when value is emitted.
Example
[Link]("Spring")
.doOnNext([Link]::println);
doOnSuccess()
Purpose
Executes action after successful completion.
Example
[Link]("Java")
.doOnSuccess(
value ->
[Link]("Success"));
doOnSubscribe()
Purpose
Executes when subscription occurs.
Example
[Link]("Java")
.doOnSubscribe(
sub ->
[Link]("Subscribed"));
doOnTerminate()
Purpose
Executes on completion or error.
Example
[Link]("Java")
.doOnTerminate(
() -> [Link]("Done"));
7. Combining Methods
Used to combine Monos.
zip()
Purpose
Combines multiple Monos.
Example
Mono<String> first =
[Link]("Java");
Mono<String> second =
[Link]("Spring");
[Link](first, second);
then()
Purpose
Ignores current value.
Example
[Link]("A")
.then([Link]("B"));
thenReturn()
Purpose
Returns specified value after completion.
Example
[Link]("Java")
.thenReturn("Completed");
8. Scheduling Methods
Used for thread management.
subscribeOn()
Purpose
Controls source execution thread.
Example
[Link]("Spring")
.subscribeOn(
[Link]());
Typical Uses
Database calls.
File operations.
Blocking APIs.
publishOn()
Purpose
Changes downstream execution thread.
Example
[Link]("Spring")
.publishOn(
[Link]());
Typical Uses
CPU-intensive tasks.
Data transformation.
9. Blocking Methods
Used mainly for testing and legacy integration.
block()
Purpose
Waits for Mono result synchronously.
Example
String result =
[Link]("Spring")
.block();
blockOptional()
Purpose
Returns Optional result.
Example
Optional<String> result =
[Link]("Java")
.blockOptional();
subscribe()
Purpose
Triggers Mono execution.
Example
[Link]("Spring")
.subscribe();
subscribe(Consumer)
Example
[Link]("Java")
.subscribe(
[Link]::println);
subscribe(Value, Error)
Example
[Link]("Java")
.subscribe(
[Link]::println,
Throwable::printStackTrace);
[Link]()
Purpose
Creates a Flux with one or more predefined values.
Use Cases
Static data.
Sample datasets.
Testing.
[Link]()
Purpose
Creates a Flux from a Collection.
Example
List<String> names =
[Link]("John", "David", "Scott");
Flux<String> flux =
[Link](names);
Use Cases
Lists.
Sets.
Database results.
[Link]()
Purpose
Creates a Flux from an array.
Example
String[] names =
{"Java", "Spring", "Reactor"};
Flux<String> flux =
[Link](names);
[Link]()
Purpose
Generates a sequence of integers.
Example
Flux<Integer> flux =
[Link](1, 5);
[Link]()
Purpose
Generates values at fixed intervals.
Example
Flux<Long> flux =
[Link]([Link](1));
Use Cases
Event streaming.
Polling services.
Real-time monitoring.
[Link]()
Purpose
Creates an empty Flux.
Example
Flux<String> flux =
[Link]();
[Link]()
Purpose
Creates a Flux that immediately emits an error.
Example
Flux<String> flux =
[Link](
new RuntimeException("Error"));
[Link]()
Purpose
Creates Flux lazily.
2. Transformation Methods
Used to transform emitted values.
map()
Purpose
Converts each element into another element.
Example
[Link]("java", "spring")
.map(String::toUpperCase);
flatMap()
Purpose
Converts each element into another Publisher.
Flattens results.
Example
[Link]("java", "spring")
.flatMap(
value ->
[Link]([Link]()));
Characteristics
Asynchronous.
concatMap()
Purpose
Similar to flatMap().
Preserves order.
Example
[Link]("A", "B", "C")
.concatMap(
value -> [Link](value));
Use Cases
Ordered processing.
Sequential execution.
flatMapSequential()
Purpose
Executes concurrently.
Benefits
Better performance than concatMap.
Ordered results.
3. Filtering Methods
Used to select required elements.
filter()
Purpose
Emits only matching elements.
Example
[Link](1,10)
.filter(i -> i % 2 == 0);
filterWhen()
Purpose
Performs asynchronous filtering.
Example
[Link]("Java","Spring")
.filterWhen(
value ->
[Link]([Link]() > 4));
distinct()
Purpose
Removes duplicate elements.
Example
[Link](1,2,2,3,3,4)
.distinct();
take()
Purpose
Takes first N elements.
Example
[Link](1,100)
.take(5);
skip()
Purpose
Skips first N elements.
Example
[Link](1,10)
.skip(5);
4. Aggregation Methods
Used to collect data.
collectList()
Purpose
Converts Flux into Mono<List<T>>.
Example
[Link]("A","B","C")
.collectList();
collectMap()
Purpose
Converts Flux into Map.
Example
[Link]("Java","Spring")
.collectMap(
value -> [Link]());
count()
Purpose
Counts emitted elements.
Example
[Link](1,10)
.count();
reduce()
Purpose
Combines all elements into one result.
Example
[Link](1,5)
.reduce((a,b) -> a+b);
5. Combining Methods
Used to combine multiple publishers.
concat()
Purpose
Sequentially combines publishers.
Example
[Link](
[Link]("A","B"),
[Link]("C","D"));
merge()
Purpose
Combines publishers concurrently.
Example
[Link](flux1, flux2);
Characteristics
Faster.
zip()
Purpose
Combines corresponding elements.
Example
[Link](
[Link]("A","B"),
[Link](1,2));
combineLatest()
Purpose
Combines latest emitted values.
Use Cases
Dashboards.
Real-time applications.
Live monitoring.
onErrorReturn()
Purpose
Returns default value on error.
Example
[Link](new RuntimeException())
.onErrorReturn("Default");
onErrorResume()
Purpose
Switches to fallback Flux.
Example
[Link](new RuntimeException())
.onErrorResume(
error ->
[Link]("Fallback"));
onErrorMap()
Purpose
Converts one exception into another.
Example
[Link](new RuntimeException())
.onErrorMap(
error ->
new IllegalArgumentException());
retry()
Purpose
Retries execution.
Example
[Link](new RuntimeException())
.retry(3);
Use Cases
Network failures.
doOnNext()
Purpose
Executes action for each emitted item.
Example
[Link]("A","B")
.doOnNext([Link]::println);
doOnSubscribe()
Purpose
Executes when subscription starts.
Example
[Link]("Java")
.doOnSubscribe(
s ->
[Link]("Subscribed"));
doOnComplete()
Purpose
Executes when stream completes.
Example
[Link]("A","B")
.doOnComplete(
() -> [Link]("Done"));
doOnError()
Purpose
Executes when error occurs.
Example
[Link](new RuntimeException())
.doOnError(
Throwable::printStackTrace);
doFinally()
Purpose
Executes regardless of outcome.
Example
[Link]("A")
.doFinally(
signal ->
[Link]("Finished"));
8. Scheduling Methods
Used for thread management.
subscribeOn()
Purpose
Determines thread for source execution.
Example
[Link](1,10)
.subscribeOn(
[Link]());
publishOn()
Purpose
Changes downstream execution thread.
Example
[Link](1,10)
.publishOn(
[Link]());
9. Backpressure Methods
Used to control data flow.
limitRate()
Purpose
Limits requested items.
Example
[Link](1,1000)
.limitRate(10);
onBackpressureBuffer()
Purpose
Buffers excess items.
Example
[Link]([Link](1))
.onBackpressureBuffer();
onBackpressureDrop()
Purpose
Drops excess items.
Example
[Link]([Link](1))
.onBackpressureDrop();
10. Subscription Methods
Used to start processing.
subscribe()
Purpose
Triggers execution.
Example
[Link]("Java")
.subscribe();
subscribe(Consumer)
Example
[Link]("Java","Spring")
.subscribe([Link]::println);
subscribe(Value, Error)
Example
[Link]("Java")
.subscribe(
[Link]::println,
Throwable::printStackTrace);
o Errors (onError)
o Cancellation
o Backpressure
o Time-based operations
It is the most commonly used testing tool for Reactor applications and Spring WebFlux
applications.
[Link]([Link]::println);
Problems
No automated verification.
Cannot validate expected values.
Cannot validate completion signals.
Cannot validate errors.
Not suitable for unit testing.
With StepVerifier:
[Link](flux)
.expectNext("Java")
.expectNext("Spring")
.verifyComplete();
Benefits
Automated assertions.
Repeatable tests.
Easy validation of reactive streams.
Better readability.
[Link](flux);
2. expectNext()
Purpose
Verifies the next emitted element.
Example
Flux<String> flux =
[Link]("Java", "Spring");
[Link](flux)
.expectNext("Java")
.expectNext("Spring")
.verifyComplete();
3. expectNextCount()
Purpose
Verifies the number of emitted items.
Does not verify actual values.
Example
Flux<Integer> flux =
[Link](1, 5);
[Link](flux)
.expectNextCount(5)
.verifyComplete();
Use Cases
Large datasets.
Performance testing.
Stream size verification.
4. expectNextMatches()
Purpose
Validates emitted values using a Predicate.
Example
[Link](
[Link]("Java"))
.expectNextMatches(
value -> [Link]("J"))
.verifyComplete();
Benefits
Flexible validation.
Complex assertions.
5. assertNext()
Purpose
Allows detailed assertions on emitted values.
Example
[Link](
[Link]("Spring"))
.assertNext(value -> {
assert [Link]("Spring");
})
.verifyComplete();
Use Cases
Multiple field validations.
Object property verification.
6. verifyComplete()
Purpose
Verifies stream completes successfully.
Example
[Link](
[Link]("A", "B"))
.expectNext("A")
.expectNext("B")
.verifyComplete();
7. expectComplete()
Purpose
Expects completion signal.
Example
[Link](
[Link]("A"))
.expectNext("A")
.expectComplete()
.verify();
9. expectError(Class)
Purpose
Verifies specific exception type.
Example
[Link](
[Link](
new IllegalArgumentException()))
.expectError(
[Link])
.verify();
10. expectErrorMessage()
Purpose
Verifies error message.
Example
[Link](
[Link](
new RuntimeException("Failed")))
.expectErrorMessage("Failed")
.verify();
11. expectErrorMatches()
Purpose
Validates exception using Predicate.
Example
[Link](
[Link](
new RuntimeException("Failed")))
.expectErrorMatches(
ex -> [Link]()
.contains("Fail"))
.verify();
Verification Methods
12. verify()
Purpose
Executes verification.
Example
[Link](
[Link]("Java"))
.expectNext("Java")
.verify();
Characteristics
Triggers subscription.
Starts verification process.
13. verify(Duration)
Purpose
Sets maximum verification time.
Example
[Link](
[Link]("Java"))
.expectNext("Java")
.verify([Link](5));
Benefits
Prevents hanging tests.
Controls execution time.
Time-Based Testing
Why Virtual Time?
Suppose a Flux emits data every 1 minute.
[Link]([Link](1))
Testing would take real time. This slows down testing.
14. withVirtualTime()
Purpose
Simulates time.
Avoids actual waiting.
Example
[Link](
() -> [Link](
[Link](5))
.take(3))
.thenAwait([Link](15))
.expectNext(0L,1L,2L)
.verifyComplete();
Benefits
Faster tests.
No [Link]().
Efficient testing.
15. thenAwait()
Purpose
Advances virtual time.
Example
.thenAwait([Link](10))
Use Cases
Delayed streams.
Scheduled tasks.
Retry operations.
Backpressure Testing
16. thenRequest()
Purpose
Requests additional items.
Example
[Link](
[Link](1,10), 2)
.expectNext(1,2)
.thenRequest(3)
.expectNext(3,4,5)
.thenCancel()
.verify();
Cancellation Testing
17. thenCancel()
Purpose
Cancels subscription.
Example
[Link](
[Link](
[Link](1)))
.expectNext(0L)
.thenCancel()
.verify();
Use Cases
Infinite streams.
Resource cleanup testing.
Without Backpressure:
Producer
│
▼
10,000 records/sec
│
▼
Consumer
100 records/sec
Problems
Data accumulates in memory.
Real-World Analogy
Restaurant Order System
Imagine:
Chef (Producer)
│
▼
Waiter (Consumer)
No overload occurs.
Publisher
Subscriber
Subscription
Processor
Publisher
│
▼
Subscription
▲
│
Subscriber
1. Buffer Strategy
Method
onBackpressureBuffer()
Behavior
Producer → Buffer → Consumer
Example
[Link]([Link](1))
.onBackpressureBuffer();
Advantages
No data loss.
Reliable delivery.
Disadvantages
Increased memory usage.
Risk of OutOfMemoryError.
2. Drop Strategy
Method
onBackpressureDrop()
Behavior
Drops excess items.
Producer
│
100 items
│
Consumer handles 20
Remaining 80 dropped
Example
[Link]([Link](1))
.onBackpressureDrop();
Advantages
Low memory usage.
High throughput.
Disadvantages
Data loss.
3. Latest Strategy
Method
onBackpressureLatest()
Behavior
Keeps only the latest item.
Example:
Received:
12345678
Consumer ready
Gets only:
8
Use Cases
Sensor readings.
Live dashboards.
Stock prices.
4. Error Strategy
Method
onBackpressureError()
Behavior
Throws exception when consumer cannot keep up.
Example
[Link]([Link](1))
.onBackpressureError();
Use Cases
Critical systems.
Request-Based Backpressure
Subscriber can request specific number of items.
Example:
request(10);
Publisher sends:
10 items only
This is called Demand-Driven Processing.
Benefits of Backpressure
Resource Protection
Prevents memory overflow.
Improved Stability
Prevents system crashes.
Maintains responsiveness.
Better Throughput
Controlled data flow.
Efficient processing.
Scalability
Handles millions of events efficiently.
Non-Blocking Communication
No thread blocking.
Event-driven processing.
Concurrency in Reactive Systems
What is Concurrency?
Concurrency is the ability of a system to handle multiple tasks, requests, or events at the
same time.
In a reactive system, concurrency is achieved without creating a dedicated thread for
every request.
Instead of blocking threads while waiting for I/O operations, reactive systems use
asynchronous, event-driven processing.
Concurrency allows applications to efficiently utilize CPU and system resources while
serving many clients simultaneously.
Scheduler-Based Concurrency
Schedulers control where tasks execute.
[Link](1, 10)
.subscribeOn([Link]())
.subscribe([Link]::println);
Purpose
Moves execution to a thread pool.
Enables concurrent processing.
Optimizes resource usage.
2. [Link]()
Characteristics
Uses one dedicated thread.
Sequential execution.
Example
[Link](1,5)
.publishOn([Link]());
Use Cases
Ordered processing.
State-sensitive operations.
3. [Link]()
Characteristics
Uses a fixed-size thread pool.
Number of threads ≈ CPU cores.
Example
[Link](1,100)
.parallel()
.runOn([Link]())
.subscribe();
Use Cases
CPU-intensive calculations.
Data transformation.
Parallel processing.
4. [Link]()
Characteristics
Dynamic thread pool.
Designed for blocking operations.
Example
[Link](this::readFile)
.subscribeOn([Link]());
Use Cases
File access.
JDBC operations.
Legacy blocking APIs.
subscribeOn() vs publishOn()
These are the most important methods for concurrency control.
subscribeOn()
Purpose
Determines where the source starts execution.
Example
[Link]("Spring")
.subscribeOn([Link]());
Flow
Source Execution
│
▼
Parallel Thread
publishOn()
Purpose
Changes thread execution for downstream operators.
Example
[Link](1,5)
.publishOn([Link]())
.map(i -> i * 10);
Flow
Source Thread
│
publishOn()
│
▼
Parallel Thread