Thinking about Quarkus usually means thinking about speed, performance, an immediate now! — but… do you think about it reactively or sequentially? Sequential thinking is the way we’re traditionally taught, while reactive thinking is how we eventually end up learning (whether we want to or not). With Mutiny, operations in Quarkus start to make sense reactively, flowing in parallel and leveraging multiple threads…
In this post, we’ll walk through the first steps with Mutiny in Quarkus, a framework designed to make our lives easier in a reactive world that can be complex and hard to grasp at first. We’ll cover everything from what Uni and Multi are to how to transform data and handle errors. Welcome to the reactive world in Quarkus.
Why use Mutiny?
1 We want (and need) to move to reactivity
The most important thing is knowing what we need, above what we want. We might want a reactive-based project when we don’t actually need one—and that would be like putting a Ferrari engine into a Seat 600.
Therefore, if we truly need reactive programming in our Quarkus project, we have to take that leap into reactivity.
2 Once decided: why Mutiny?
- Mutiny is the “default” reactive API in Quarkus, since many extensions and libraries (HTTP, REST, clients, etc.) already expose their APIs by returning Uni or Multi, as shown in the official Quarkus documentation.
- At the same time, Mutiny allows us to work with event-driven code in a declarative way—something like: “when this happens, do that.” This avoids manual callbacks or sequential code blocks.
- It provides a clear API with chainable operators, improving readability compared to complex chains of generic operators.
- In environments such as GraalVM, Quarkus together with Mutiny helps eliminate unused code (“dead-code elimination”).
3 Key concepts: Uni and Multi
Mutiny revolves around two key concepts:
- Uni
: represents an operation that emits one or no result, or fails in case of an error. The equivalent in RxJava would be Mono . - Multi
: represents a stream of multiple values (0..N) or an error if something goes wrong. It is similar to a Stream in sequential Java and to a Flux in reactive programming. It’s especially useful for data streams, streaming use cases, etc.
Once we’re clear on what each one represents, how do we work with them? What operations can we perform?
The easiest way is to look at an example:
Uni.createFrom().item("Hello")
.onItem()
.transform(s -> s + " world")
.onItem()
.invoke(s->System.out.println("Broadcast"+s))
.onFailure().recoverWithItem("Error over here");
In that code, the following happens:
- We create a Uni (createFrom) that will emit Hello when the time comes.
- When the item arrives, we transform it (transform) into “Hello world”.
- We execute a side effect (invoke) to print the value.
- In case of failure, a fallback string “Error over here” is returned (onFailure().recoverWithItem).
Therefore, the most common operators in Mutiny are:
- onItem() → an operator used to act on the emitted value, whether to transform it, filter it, etc.
- onFailure() → used to handle errors that may occur during the operation.
- combine() → used to merge multiple streams. For example:
Uni.combine().all()unis(...).combinedWith(...)
Other operators that we frequently use with Multi are already well known from Java, such as merge(), concatenate(), select(), and many more that you can find here.
4 Preparing our Quarkus project for Mutiny
Once we’ve decided that we really do need Mutiny and reactivity in our Quarkus project, we’ll go step by step (starting from project creation):
- Generate our Quarkus project, either via code.quarkus.io or using mvn.
- code.quarkus.io → Fill in the basic details and add the following extensions: Mutiny and RESTEasy Reactive. Then download the project and import it into your IDE.
- Using mvn → We can use the following command from the terminal:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=mutiny-demo \
-DclassName="com.example.GreetingResource" \
-Dpath="/hello"
- If the Mutiny dependency is not present, we add it manually to our pom:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny</artifactId>
</dependency>
Or via mvn:
mvn quarkus:add-extension -Dextensions=mutiny
- Once the project has been generated, we are going to create a basic endpoint that returns a greeting, as one should do when seeing someone:
package com.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import io.smallrye.mutiny.Uni;
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> hello() {
return Uni.createFrom().item("Hello from Quarkus + Mutiny")
.onItem().transform(s -> s + " — effective as of " + System.currentTimeMillis());
}
}
- In this code: we create a Uni target (createFrom()) to which we assign a string value (.item): “Hello from Quarkus + Mutiny”, and we use onItem().transform() to transform that string into a new combined one and return it.
- When we call that endpoint (in our case, curl localhost:8080/hello), it returns something like: “Hello from Quarkus + Mutiny — valid at 169xxx…”.
And just like that, we’ve built our very first Mutiny endpoint with Quarkus!
5 Using Multi for streams and lists
In our first endpoint, we relied solely on Uni
If we had an endpoint called words:
@Path("/words")
public class WordsResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Multi<String> words() {
return Multi.createFrom().items("one", "two", "three")
.onItem().transform(String::toUpperCase);
}
}
When we make the request (localhost:8080/words), Quarkus will send us a JSON stream (if our client/server supports it). You can also combine Multi with Uni to transform each element of our Multi into an individual Uni:
Multi.createFrom().items("a", "b", "c")
.onItem().transformToUni(s -> Uni.createFrom().item(s + "_x"))
.concatenate();
Another example we can find with Multis is the combination between them, that is, all to the Multi!
public Uni<String> combined() {
Uni<String> u1 = Uni.createFrom().item("A").onItem().delayIt().by(Duration.ofMillis(100));
Uni<String> u2 = Uni.createFrom().item("B").onItem().delayIt().by(Duration.ofMillis(200));
return Uni.combine().all().unis(u1, u2)
.combinedWith(list -> {
String s1 = (String) list.get(0);
String s2 = (String) list.get(1);
return s1 + "-" + s2;
});
}
In this example, combined() waits for both operations to complete and then combines them. It does not return a result until both have finished.
6 Common mistakes and how to avoid them
- Not subscribing / returning incorrectly
In Quarkus, if we define an endpoint that returns a Uni (for example, the GET endpoint used in the examples), Quarkus automatically takes care of subscribing. That is, when the Uni emits a value, Quarkus transforms it into an HTTP response (for example, a 200 OK with the content of the Uni). Do not use subscribe() manually outside of testing contexts.
- Blocking I/O threads
We should not use blocking calls inside our applications (Thread.sleep, blocking database access, etc.). If we need to perform a blocking operation, the best approach is to convert it into a Uni executed on a different thread or use .runSubscriptionOn(...).
- Using mutable state
We must avoid mutating lists or external variables inside operators, as this can lead to unexpected results in a reactive environment.
- Mixing asynchronous and synchronous code
Always design operations from the beginning using Uni and Multi, ensuring that everything either returns these types or works directly with them.
- Tests, tests, and more tests!
Mutiny has strong testing support—so let’s make use of it.
Conclusion
We now have our first foundations to take another step into reactive programming (as we’ve already seen in other posts focused on RxJava) but this time with Quarkus.
We’ve met Multi and Uni, our new friends, and seen how to combine them in different operations. If this has been your first contact with reactive programming, you probably felt the jump more strongly, as it forces us to change the way we think.
However, one of the most important things we need to do is try and fail, fail and try again. That’s how we truly learn reactive programming and make it feel familiar. Looking forward to your thoughts in the comments!
References
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.