Wenn Java-Entwickler an Microservices denken, fallen zuerst Quarkus und Micronaut. Doch seit Version 4 hat Oracle’s Helidon einen bemerkenswerten Wandel vollzogen: Es ist das erste Java-Webframework, das vollständig auf Virtual Threads (Project Loom) basiert — und bietet damit eine erfrischende Alternative zum reaktiven Programmiermodell.

Dieser Artikel zeigt dir den Einstieg in Helidon 4.5 und erklärt, warum der „Blocking-Code auf Virtual Threads“-Ansatz das Leben von Microservice-Entwicklern einfacher macht.

Was ist Helidon?

Helidon (griechisch für Schwalbe — ein kleiner, wendiger Vogel) gibt es in zwei Flavors:

FlavorBeschreibung
Helidon SEFunktionaler, expliziter Stil ohne Framework-Magie. Kleinster Footprint (~34 MB native).
Helidon MPMicroProfile-kompatibel mit CDI, JAX-RS, JSON-P/B. Für Jakarta-EE-erfahrene Teams.

Beide teilen sich den Helidon WebServer als Kern und unterstützen Java 21+ sowie GraalVM Native Image. Der aktuelle Release ist Helidon 4.5.0 (Juni 2025).

Der Clou: Virtual Threads statt Reactive

Das reaktive Programmierparadigma (Project Reactor, RxJava, Mutiny) hat ein echtes Problem: Der Code wird schwer lesbar, Stacktraces werden unbrauchbar, und Debugging wird zur Qual. Helidon 4 löst das anders: Es verwendet Java Virtual Threads, die bei I/O-Operationen automatisch vom JVM-Scheduler suspendiert werden, ohne Plattform-Threads zu blockieren.

Das Resultat: Du schreibst ganz normalen, blockierenden Code — und die JVM macht ihn hochskalierend.

<em>// In Spring WebFlux (reaktiv):</em>
return userRepository.findById(id)
    .flatMap(user -> orderService.findOrders(user))
    .map(orders -> new UserWithOrders(user, orders));

<em>// In Helidon (blockierend auf Virtual Threads):</em>
var user = userRepository.findById(id);
var orders = orderService.findOrders(user);
return new UserWithOrders(user, orders);
Code-Sprache: JavaScript (javascript)

Beide Varianten skalieren auf Millionen gleichzeitiger Verbindungen — aber nur eine liest sich wie normaler Java-Code.

Quickstart: ein REST-Endpunkt mit Helidon SE

Projekt mit der Helidon CLI erstellen:

curl -L -O https://helidon.io/cli/latest/darwin/helidon
chmod +x helidon && sudo mv helidon /usr/local/bin/

helidon init --batch -Dflavor=se -Dapp-type=quickstart
cd quickstart-se && mvn clean package -Pjre
java -jar target/quickstart-se.jar
Code-Sprache: JavaScript (javascript)

Die generierte Main.java:

import io.helidon.webserver.WebServer;
import io.helidon.webserver.http.HttpRouting;

public class Main {
    public static void main(String[] args) {
        WebServer.builder()
            .port(8080)
            .addRouting(HttpRouting.builder()
                .get("/greet", (req, res) -> res.send("Hallo Helidon!"))
                .get("/greet/{name}", (req, res) -> {
                    String name = req.path().pathParameters().get("name");
                    res.send("Hallo " + name + "!");
                })
            )
            .build()
            .start();
    }
}
Code-Sprache: JavaScript (javascript)

Noch simpler geht’s kaum. Der Server startet in ~0,6 Sekunden, und jede Anfrage läuft auf einem eigenen Virtual Thread.

Eine kleine REST-API mit Services

So sieht eine vollständige Todo-API mit Dependency Injection (Helidon Inject) und JSON-Serialisierung aus:

<em>// Todo.java — Modell</em>
public record Todo(String id, String task, boolean done) {}

<em>// TodoService.java — Business-Logik</em>
@Singleton
public class TodoService {
    private final Map<String, Todo> todos = new ConcurrentHashMap<>();

    public TodoService() {
        add(new Todo("1", "Helidon lernen", false));
    }

    public List<Todo> list() {
        return List.copyOf(todos.values());
    }

    public Todo add(Todo todo) {
        todos.put(todo.id(), todo);
        return todo;
    }
}
Code-Sprache: PHP (php)
<em>// TodoRoutes.java — HTTP-Endpunkte</em>
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;
import jakarta.json.Json;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObject;

@Singleton
public class TodoRoutes implements HttpService {

    private final TodoService service;

    @Inject
    public TodoRoutes(TodoService service) {
        this.service = service;
    }

    @Override
    public void routing(HttpRules rules) {
        rules.get("/todos", this::getAll)
               .post("/todos", this::create);
    }

    private void getAll(ServerRequest req, ServerResponse res) {
        var json = service.list().stream()
            .map(t -> Json.createObjectBuilder()
                .add("id", t.id())
                .add("task", t.task())
                .add("done", t.done())
                .build())
            .collect(Json.createArrayBuilder(), JsonArrayBuilder::add, (a, b) -> {})
            .build();
        res.header("Content-Type", "application/json").send(json);
    }

    private void create(ServerRequest req, ServerResponse res) {
        JsonObject body = req.content().as(JsonObject.class);
        Todo todo = service.add(new Todo(
            UUID.randomUUID().toString(),
            body.getString("task"),
            false
        ));
        res.status(201).send(todo);
    }
}
Code-Sprache: JavaScript (javascript)

Virtual-Thread-Architektur unter der Haube

Helidon 4’s Server (ursprünglich unter dem Codenamen „Níma“ entwickelt) verwendet folgendes Thread-Modell:

  1. Socket-Listener laufen auf Plattform-Threads (ein Thread pro Server-Socket)
  2. HTTP/1.1: Ein Virtual Thread pro Verbindung (bearbeitet Routing + alle Requests)
  3. HTTP/2: Ein Virtual Thread pro Stream — jeder Stream bekommt Routing auf einem eigenen Thread
  4. Der JVM-Scheduler multiplexiert Millionen Virtual Threads auf wenige OS-Threads

Das bedeutet: Du kannst Code wie Thread.sleep() oder blockingRepository.findById() ganz normal aufrufen, ohne Angst vor Thread-Pool-Erschöpfung. Der Virtual Thread wird suspendiert, der Carrier-Thread arbeitet weiter an einem anderen Virtual Thread, und sobald das I/O bereit ist, läuft dein Code weiter.

Performance & GraalVM Native Image

Helidon SE glänzt mit beeindruckenden Werten:

MetrikJVMNative Image
Speicher (RSS)~70 MB~34 MB
Startup-Zeit0,6 s0,06 s
Artefaktgröße333 MB38 MB

Mit GraalVM Native Image lassen sich sub-100ms Startup-Zeiten erreichen — ideal für Serverless und Auto-Scaling. Das native Image baust du mit:

mvn package -Pnative

Helidon SE vs. Helidon MP: Wann welchen Flavor?

  • Helidon SE eignet sich für Teams, die maximale Kontrolle und minimalen Footprint wollen. Keine Annotation-Magie, kein CDI-Overhead, direkte APIs.
  • Helidon MP ist ideal für Teams mit Jakarta-EE-Hintergrund oder wenn MicroProfile-Standards (Health, Metrics, OpenAPI, Config) gefordert sind.

Maven BOM für eigene Projekte

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.helidon</groupId>
            <artifactId>helidon-bom</artifactId>
            <version>4.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
Code-Sprache: HTML, XML (xml)

Fazit

Helidon 4 beweist, dass Java-Microservices nicht reaktiv sein müssen, um hochskalierend zu sein. Virtual Threads erlauben sauberen, debuggaren Code mit vergleichbarer Performance. Besonders Helidon SE besticht durch seine Einfachheit: Keine Reflexion, keine Annotation-Prozessoren, keine Framework-Magie — nur Java, so wie du es kennst.

Für den nächsten Microservice solltet ihr Helidon definitiv auf der Shortlist haben.