1. Introduction
Mono is a central concept in reactive programming, particularly in Project Reactor. It represents a stream that emits at most one item and then completes successfully or with an error. Mono is used for asynchronous operations that return a single result or no result at all.
Mono is particularly useful for representing the result of asynchronous computations, such as database queries, HTTP requests, or any operation that returns a single value or completes without emitting any value.
In this article, we’ll explore the differences between three common ways to create a Mono: just(), defer(), and create().
2. Mono.just()
The Mono.just() method is the simplest way to create a Mono. It takes an existing value and wraps it in a Mono, effectively making it available for subscribers immediately. We use this method when data is available and there’s no need for complex computation or deferred execution.
When we use Mono.just(), the value is evaluated and stored at the moment the Mono is created, rather than waiting until it’s subscribed to. Once created, the value cannot be changed, even if the underlying data source is modified before subscription. This ensures that the value emitted during subscription is always the one that was available at the time of Mono creation.
Let’s see how this behavior works with an example:
@Test
void whenUsingMonoJust_thenValueIsCreatedEagerly() {
String[] value = {"Hello"};
Mono<String> mono = Mono.just(value[0]);
value[0] = "world";
mono.subscribe(actualValue -> assertEquals("Hello", actualValue));
}
In this example, we can see that Mono.just() creates the value eagerly at the time of Mono creation. The Mono is initialized with the value “Hello” from an array, and even though the array element is modified to “world” before the Mono is subscribed to, the emitted value remains “Hello”.
Let’s explore few common use cases for Mono.just():
- When we have a known, static value that’s ready to emit
- It’s ideal for simple use cases that don’t involve any computation or side effects
3. Mono.defer()
Mono.defer() enables lazy execution, meaning the Mono isn’t created until it’s subscribed to. This is especially useful when we want to avoid unnecessary resource allocation or computation until the Mono is actually needed.
Let’s examine an example to verify that the Mono is created when it’s subscribed to, not when it’s initially defined:
@Test
void whenUsingMonoDefer_thenValueIsCreatedLazily() {
String[] value = {"Hello"};
Mono<String> mono = Mono.defer(() -> Mono.just(value[0]));
value[0] = "World";
mono.subscribe(actualValue -> assertEquals("World", actualValue));
}
In this example, we can see that the value in the array is changed after the Mono is created. Since Mono.defer() creates the Mono lazily, it doesn’t capture the value at creation time but instead waits until the Mono is subscribed to. As a result, when we subscribe, we receive the updated value, “World”, rather than the original value, “Hello”, demonstrating that Mono.defer() defers the evaluation until the moment of subscription.
Mono.defer() is particularly useful in scenarios where we need to create a new, distinct Mono instance for each subscriber. This allows us to generate a separate instance that reflects the current state or data at the moment of subscription. This approach is essential when we need to dynamically derive the value based on conditions that might change between subscriptions.
Let’s look into one scenario where deferred Mono is created based on the method parameter:
public Mono<String> getGreetingMono(String name) {
return Mono.defer(() -> {
String greeting = "Hello, " + name;
return Mono.just(greeting);
});
}
Here, the Mono.defer() method defers the creation of the Mono until the subscription, using the name passed to the getGreetingMono() method:
@Test
void givenNameIsAlice_whenMonoSubscribed_thenShouldReturnGreetingForAlice() {
Mono<String> mono = generator.getGreetingMono("Alice");
StepVerifier.create(mono)
.expectNext("Hello, Alice")
.verifyComplete();
}
@Test
void givenNameIsBob_whenMonoSubscribed_thenShouldReturnGreetingForBob() {
Mono<String> mono = generator.getGreetingMono("Bob");
StepVerifier.create(mono)
.expectNext("Hello, Bob")
.verifyComplete();
}
When called with the argument “Alice”, the method produces a Mono that emits “Hello, Alice”. Similarly, when called with “Bob”, it produces a Mono that emits “Hello, Bob”.
Let’s see some common use cases for Mono.defer():
- When our Mono creation involves expensive operations like database queries or network calls, using defer(), we can prevent these from being executed unless this result is actually needed
- It’s useful for generating values dynamically or when the computation depends on external factors or user input
4. Mono.create()
Mono.create() is the most flexible and powerful method, giving us full control over the Mono‘s emission process. It allows us to programmatically generate values, signals, and errors based on custom logic.
The key feature is that it provides a MonoSink. We can use the sink to emit a value with sink.success(). If there’s an error, we use sink.error(). To send a completion signal without a value, we call sink.success() without arguments.
Let’s explore an example where we perform an external operation, such as querying a remote service. In this scenario, we’ll use Mono.create() to encapsulate the logic for handling the response. Depending on whether the service call is successful or results in an error, we’ll emit either a valid value or an error signal to the subscribers:
public Mono<String> performOperation(boolean success) {
return Mono.create(sink -> {
if (success) {
sink.success("Operation Success");
} else {
sink.error(new RuntimeException("Operation Failed"));
}
});
}
Let’s verify that this Mono emits a success message when the operation succeeds:
@Test
void givenSuccessScenario_whenMonoSubscribed_thenShouldReturnSuccessValue() {
Mono<String> mono = generator.performOperation(true);
StepVerifier.create(mono)
.expectNext("Operation Success")
.verifyComplete();
}
When the operation fails, the Mono emits an error with the message “Operation Failed“:
@Test
void givenErrorScenario_whenMonoSubscribed_thenShouldReturnError() {
Mono<String> mono = generator.performOperation(false);
StepVerifier.create(mono)
.expectErrorMatches(throwable -> throwable instanceof RuntimeException
&& throwable.getMessage()
.equals("Operation Failed"))
.verify();
}
Let’s explore a few common scenarios for Mono.create():
- This method provides the necessary flexibility when we need to implement complex logic for emitting values, handling errors, or managing back-pressure
- It’s ideal for integrating with legacy systems or performing complex asynchronous tasks that require fine-grained control over emission and completion
5. Key Differences
Now that we’ve explored the three methods individually, let’s summarize their key differences:
Feature | Mono.just() | Mono.defer() | Mono.create() |
---|---|---|---|
Execution Time | Eager (upon creation) | Lazy (upon subscription) | Lazy (manual emission) |
Value State |
Static/predefined value |
Dynamic value on each subscription | Manually produced value |
Use case | When data is available and doesn’t change |
When data needs to be generated on demand |
When integrating with complex logic or external sources |
Error Handling | No error handling |
Can handle errors during Mono creation |
Explicit control over success and errors |
Performance |
Efficient for static, already-known values |
Useful for dynamic or expensive operations |
Suitable for complex or asynchronous code |
6. Conclusion
In this tutorial, we examined three methods for creating Mono, including scenarios where each approach is most suitable.
Understanding the differences between Mono.just(), Mono.defer(), and Mono.create() is key to effectively using reactive programming in Java. By choosing the right approach, we can make our reactive code more efficient, maintainable, and suited to our specific use case.
As always, the source code for this article is available over on GitHub.