Execution Modes & Return Types

The extension supports several return types, each with different execution semantics. You can further control threading with the @Blocking and @NonBlocking annotations.

Synchronous Methods

Plain return types run on a worker thread by default (blocking):

@JsonRPCApi
public class MyService {

    // Runs on executor-thread (blocking, default)
    public String compute(String input) {
        return expensiveOperation(input);
    }

    // Runs on vert.x-eventloop-thread (non-blocking)
    @NonBlocking
    public String computeFast(String input) {
        return cheapOperation(input);
    }
}
Return type Default execution

Plain type (String, int, POJO, …​)

Blocking — worker thread via vertx.executeBlocking()

Plain type + @NonBlocking

Non-blocking — Vert.x event loop

Uni (Async Single Result)

Uni<T> from Mutiny returns a single async result:

@JsonRPCApi
public class MyService {

    // Non-blocking by default
    public Uni<String> fetchData(String id) {
        return dataClient.get(id);
    }

    // Force blocking execution
    @Blocking
    public Uni<String> fetchDataBlocking(String id) {
        return Uni.createFrom().item(blockingLookup(id));
    }
}
Return type Default execution

Uni<T>

Non-blocking — event loop

Uni<T> + @Blocking

Blocking — wrapped in vertx.executeBlocking()

CompletionStage / CompletableFuture

Standard Java async types are also supported and behave like Uni<T>:

@JsonRPCApi
public class MyService {

    public CompletionStage<String> fetchAsync(String id) {
        return CompletableFuture.supplyAsync(() -> lookup(id));
    }
}

Multi (Streaming)

Multi<T> enables server-sent streaming via JSON-RPC subscriptions. See Streaming with Multi for the full protocol details.

@JsonRPCApi
public class MyService {

    public Multi<String> ticker() {
        return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
                .onItem().transform(n -> "tick " + n);
    }
}

Flow.Publisher<T> is also supported and treated identically to Multi<T>.

Virtual Threads

If your application runs on Java 21+, you can use @RunOnVirtualThread to execute methods on virtual threads instead of the platform worker thread pool. This gives you the simple blocking programming model with much better scalability — virtual threads are cheap and plentiful, so you won’t exhaust a fixed thread pool under load.

@JsonRPCApi
public class MyService {

    // Runs on a virtual thread
    @RunOnVirtualThread
    public String fetchData(String id) {
        return blockingHttpClient.get(id); // blocking I/O is fine here
    }

    // Also works with Uni
    @RunOnVirtualThread
    public Uni<String> fetchDataUni(String id) {
        return Uni.createFrom().item(blockingHttpClient.get(id));
    }
}

To use @RunOnVirtualThread, add the quarkus-virtual-threads extension to your project:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-virtual-threads</artifactId>
</dependency>
Combination Behavior

@RunOnVirtualThread

Executes on a virtual thread

@RunOnVirtualThread + @Blocking

Executes on a virtual thread (@RunOnVirtualThread takes precedence)

@RunOnVirtualThread + @NonBlocking

Build error — these are contradictory

Summary

Return type Default Override

Plain type

Blocking

@NonBlocking, @RunOnVirtualThread

Uni<T>

Non-blocking

@Blocking, @RunOnVirtualThread

CompletionStage<T>

Non-blocking

@Blocking, @RunOnVirtualThread

Multi<T>

Non-blocking

Flow.Publisher<T>

Non-blocking

A method cannot be annotated with both @Blocking and @NonBlocking, or both @RunOnVirtualThread and @NonBlocking. The build will fail if conflicting annotations are present.