Helidon SE, with its lightweight microservices framework, combined with GraalVM Native Image, creates an ideal platform for building cloud-native applications with minimal memory footprint and instant startup times. This article provides a complete guide to building, configuring, and optimizing Helidon SE applications as native executables using GraalVM Native Image.
Why Helidon SE with Native Image?
Benefits:
- Instant Startup: Sub-second startup times (typically 10-50ms)
- Minimal Memory: As low as 20-30MB RAM vs 200-300MB with JVM
- Reduced Attack Surface: No JVM, smaller container images
- Perfect for Serverless: Ideal for FaaS environments with cold starts
- Container-Friendly: Tiny Docker images (~50MB vs ~300MB)
Project Setup and Dependencies
Maven Configuration:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>helidon-native-demo</artifactId> <version>1.0.0</version> <properties> <helidon.version>3.2.2</helidon.version> <graalvm.version>23.0.0</graalvm.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- Native Image Configuration --> <native.image.maven.plugin.version>0.9.23</native.image.maven.plugin.version> <exec.mainClass>com.example.Main</exec.mainClass> </properties> <dependencies> <!-- Helidon SE WebServer --> <dependency> <groupId>io.helidon.webserver</groupId> <artifactId>helidon-webserver</artifactId> <version>${helidon.version}</version> </dependency> <!-- Helidon Config --> <dependency> <groupId>io.helidon.config</groupId> <artifactId>helidon-config</artifactId> <version>${helidon.version}</version> </dependency> <!-- JSON Support --> <dependency> <groupId>io.helidon.media</groupId> <artifactId>helidon-media-jsonp</artifactId> <version>${helidon.version}</version> </dependency> <!-- Jackson for JSON (if needed) --> <dependency> <groupId>io.helidon.media</groupId> <artifactId>helidon-media-jackson</artifactId> <version>${helidon.version}</version> </dependency> <!-- Health and Metrics --> <dependency> <groupId>io.helidon.health</groupId> <artifactId>helidon-health</artifactId> <version>${helidon.version}</version> </dependency> <dependency> <groupId>io.helidon.metrics</groupId> <artifactId>helidon-metrics</artifactId> <version>${helidon.version}</version> </dependency> </dependencies> <build> <plugins> <!-- Native Image Maven Plugin --> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>${native.image.maven.plugin.version}</version> <extensions>true</extensions> <executions> <execution> <id>build-native</id> <goals> <goal>compile-no-fork</goal> </goals> <phase>package</phase> </execution> </executions> <configuration> <mainClass>${exec.mainClass}</mainClass> <buildArgs> <buildArg>--verbose</buildArg> <buildArg>-H:EnableURLProtocols=http,https</buildArg> <buildArg>-H:+ReportExceptionStackTraces</buildArg> <buildArg>--initialize-at-build-time=io.helidon</buildArg> </buildArgs> <imageName>helidon-native-app</imageName> </configuration> </plugin> <!-- Standard Java Compiler --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> <!-- Create Fat JAR for comparison --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>${exec.mainClass}</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> Basic Helidon SE Application
Simple REST Service:
package com.example; import io.helidon.webserver.*; import io.helidon.webserver.http.*; import io.helidon.config.Config; import io.helidon.media.jsonp.JsonpSupport; import javax.json.Json; import javax.json.JsonObject; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public class Main { private static final AtomicLong ID_GENERATOR = new AtomicLong(1); private static final ConcurrentHashMap<Long, User> USERS = new ConcurrentHashMap<>(); public static void main(String[] args) { startServer(); } public static WebServer startServer() { Config config = Config.create(); WebServer server = WebServer.builder() .config(config.get("server")) .port(8080) .addMediaSupport(JsonpSupport.create()) .routing(routing -> routing .register("/api", new UserService()) .get("/health", (req, res) -> { JsonObject health = Json.createObjectBuilder() .add("status", "UP") .add("timestamp", System.currentTimeMillis()) .build(); res.send(health); }) .get("/", (req, res) -> { JsonObject info = Json.createObjectBuilder() .add("name", "Helidon Native Demo") .add("version", "1.0.0") .add("native", true) .build(); res.send(info); }) ) .build(); server.start() .thenAccept(ws -> { System.out.println("🚀 Server started on http://localhost:" + ws.port()); System.out.println("📊 Health check: http://localhost:" + ws.port() + "/health"); System.out.println("👤 Users API: http://localhost:" + ws.port() + "/api/users"); }) .exceptionally(t -> { System.err.println("Failed to start server: " + t.getMessage()); t.printStackTrace(); return null; }); // Add shutdown hook Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Shutting down server..."); server.shutdown(); })); return server; } // User model public static class User { private final Long id; private String name; private String email; public User(String name, String email) { this.id = ID_GENERATOR.getAndIncrement(); this.name = name; this.email = email; } // Getters and setters public Long getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public JsonObject toJson() { return Json.createObjectBuilder() .add("id", id) .add("name", name) .add("email", email) .build(); } } // User service with REST endpoints public static class UserService implements HttpService { @Override public void update(Routing.Rules rules) { rules.get("/users", this::listUsers) .get("/users/{id}", this::getUser) .post("/users", this::createUser) .put("/users/{id}", this::updateUser) .delete("/users/{id}", this::deleteUser); } private void listUsers(ServerRequest req, ServerResponse res) { JsonObject response = Json.createObjectBuilder() .add("users", USERS.values().stream() .map(User::toJson) .collect(Json.createArrayBuilder::add, JsonArrayBuilder::add, JsonArrayBuilder::add) .build()) .build(); res.send(response); } private void getUser(ServerRequest req, ServerResponse res) { try { Long id = Long.parseLong(req.path().pathParameters().get("id")); User user = USERS.get(id); if (user == null) { res.status(404).send(Json.createObjectBuilder() .add("error", "User not found") .build()); return; } res.send(user.toJson()); } catch (NumberFormatException e) { res.status(400).send(Json.createObjectBuilder() .add("error", "Invalid user ID") .build()); } } private void createUser(ServerRequest req, ServerResponse res) { req.content().as(JsonObject.class).thenAccept(json -> { String name = json.getString("name"); String email = json.getString("email"); User user = new User(name, email); USERS.put(user.getId(), user); res.status(201).send(user.toJson()); }).exceptionally(t -> { res.status(400).send(Json.createObjectBuilder() .add("error", "Invalid JSON") .build()); return null; }); } private void updateUser(ServerRequest req, ServerResponse res) { try { Long id = Long.parseLong(req.path().pathParameters().get("id")); User user = USERS.get(id); if (user == null) { res.status(404).send(Json.createObjectBuilder() .add("error", "User not found") .build()); return; } req.content().as(JsonObject.class).thenAccept(json -> { if (json.containsKey("name")) { user.setName(json.getString("name")); } if (json.containsKey("email")) { user.setEmail(json.getString("email")); } res.send(user.toJson()); }); } catch (NumberFormatException e) { res.status(400).send(Json.createObjectBuilder() .add("error", "Invalid user ID") .build()); } } private void deleteUser(ServerRequest req, ServerResponse res) { try { Long id = Long.parseLong(req.path().pathParameters().get("id")); User removed = USERS.remove(id); if (removed == null) { res.status(404).send(Json.createObjectBuilder() .add("error", "User not found") .build()); return; } res.status(204).send(); } catch (NumberFormatException e) { res.status(400).send(Json.createObjectBuilder() .add("error", "Invalid user ID") .build()); } } } } Native Image Configuration
Reflection Configuration (reflect-config.json):
[ { "name": "com.example.Main$User", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true, "allDeclaredFields": true, "allPublicFields": true }, { "name": "java.util.concurrent.ConcurrentHashMap", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true } ] Resource Configuration (resource-config.json):
{ "resources": { "includes": [ { "pattern": "application\\.properties$" }, { "pattern": "META-INF/services/.*" }, { "pattern": ".*\\.json$" } ] } } Native Image Properties (native-image.properties):
Args = --initialize-at-build-time=io.helidon \ --initialize-at-build-time=org.jboss.logging \ -H:EnableURLProtocols=http,https \ -H:+ReportExceptionStackTraces \ -H:+ReportUnsupportedElementsAtRuntime \ -H:+AddAllCharsets \ --allow-incomplete-classpath \ --report-unsupported-elements-at-runtime \ --no-fallback \ --no-server
Advanced Configuration with Health and Metrics
Enhanced Application with Health Checks:
package com.example.advanced; import io.helidon.webserver.*; import io.helidon.webserver.http.*; import io.helidon.config.Config; import io.helidon.media.jsonp.JsonpSupport; import io.helidon.health.HealthSupport; import io.helidon.health.checks.HealthChecks; import io.helidon.metrics.MetricsSupport; import javax.json.Json; import javax.json.JsonObject; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.util.concurrent.atomic.AtomicBoolean; public class AdvancedNativeApp { private static final AtomicBoolean READY = new AtomicBoolean(true); private static final AtomicBoolean LIVE = new AtomicBoolean(true); public static void main(String[] args) { startServer(); } public static WebServer startServer() { Config config = Config.create(); // Health checks HealthSupport health = HealthSupport.builder() .addLiveness(HealthChecks.healthChecks()) .addReadiness(() -> { boolean ready = READY.get(); return ready ? HealthCheckResponse.ok() : HealthCheckResponse.nok("Service not ready"); }) .addLiveness(() -> { boolean live = LIVE.get(); return live ? HealthCheckResponse.ok() : HealthCheckResponse.nok("Service not live"); }) .webContext("/health") .build(); // Metrics MetricsSupport metrics = MetricsSupport.create(); WebServer server = WebServer.builder() .config(config.get("server")) .port(8080) .addMediaSupport(JsonpSupport.create()) .routing(routing -> routing .register(health) // /health/ready, /health/live .register(metrics) // /metrics .register("/api", new ApiService()) .get("/", (req, res) -> { JsonObject info = Json.createObjectBuilder() .add("application", "Helidon Native Advanced") .add("native", true) .add("startupTime", ManagementFactory.getRuntimeMXBean().getStartTime()) .add("uptime", ManagementFactory.getRuntimeMXBean().getUptime()) .build(); res.send(info); }) .get("/memory", (req, res) -> { MemoryMXBean memory = ManagementFactory.getMemoryMXBean(); JsonObject memoryInfo = Json.createObjectBuilder() .add("heapUsed", memory.getHeapMemoryUsage().getUsed()) .add("heapMax", memory.getHeapMemoryUsage().getMax()) .add("nonHeapUsed", memory.getNonHeapMemoryUsage().getUsed()) .build(); res.send(memoryInfo); }) .post("/control/ready/{state}", (req, res) -> { String state = req.path().pathParameters().get("state"); READY.set("true".equalsIgnoreCase(state)); res.send(Json.createObjectBuilder() .add("ready", READY.get()) .build()); }) .post("/control/live/{state}", (req, res) -> { String state = req.path().pathParameters().get("state"); LIVE.set("true".equalsIgnoreCase(state)); res.send(Json.createObjectBuilder() .add("live", LIVE.get()) .build()); }) ) .build(); server.start() .thenAccept(ws -> { System.out.println("🚀 Advanced Native Server started!"); System.out.println("📍 Port: " + ws.port()); System.out.println("❤️ Health: http://localhost:" + ws.port() + "/health"); System.out.println("📊 Metrics: http://localhost:" + ws.port() + "/metrics"); System.out.println("🎛️ Control: http://localhost:" + ws.port() + "/control/ready/{true|false}"); }); return server; } public static class ApiService implements HttpService { @Override public void update(Routing.Rules rules) { rules.get("/info", this::getInfo) .get("/status", this::getStatus); } private void getInfo(ServerRequest req, ServerResponse res) { Runtime runtime = Runtime.getRuntime(); JsonObject info = Json.createObjectBuilder() .add("native", true) .add("availableProcessors", runtime.availableProcessors()) .add("freeMemory", runtime.freeMemory()) .add("maxMemory", runtime.maxMemory()) .add("totalMemory", runtime.totalMemory()) .add("ready", READY.get()) .add("live", LIVE.get()) .build(); res.send(info); } private void getStatus(ServerRequest req, ServerResponse res) { JsonObject status = Json.createObjectBuilder() .add("status", "operational") .add("timestamp", System.currentTimeMillis()) .add("version", "1.0.0-native") .build(); res.send(status); } } } Building and Running
Build Commands:
# Install GraalVM and setup environment export GRAALVM_HOME=/path/to/graalvm export PATH=$GRAALVM_HOME/bin:$PATH # Verify installation java -version native-image --version # Build native image mvn clean package -Pnative # Alternative: Explicit native build mvn native:compile -Pnative # Build for specific platform mvn package -Pnative -Dnative.buildArgs="--static -H:NativeLinkerOption=-s"
Docker Configuration:
Dockerfile:
FROM oraclelinux:9-slim # Install required packages RUN microdnf update -y && \ microdnf install -y gcompat && \ microdnf clean all # Create non-root user RUN groupadd -r appuser && useradd -r -g appuser appuser USER appuser # Copy native executable COPY target/helidon-native-app /app/helidon-native-app # Expose port EXPOSE 8080 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Run application CMD ["/app/helidon-native-app"]
Multi-stage Dockerfile:
# Build stage FROM ghcr.io/graalvm/native-image:ol8-java17-22 AS builder WORKDIR /build COPY . . RUN mvn clean package -Pnative -DskipTests # Runtime stage FROM oraclelinux:9-slim RUN microdnf update -y && microdnf install -y gcompat && microdnf clean all RUN groupadd -r appuser && useradd -r -g appuser appuser USER appuser COPY --from=builder /build/target/helidon-native-app /app/ EXPOSE 8080 CMD ["/app/helidon-native-app"]
Build Script (build.sh):
#!/bin/bash set -e echo "🔨 Building Helidon SE Native Image..." # Clean previous builds mvn clean # Build JAR first for testing echo "📦 Building JAR..." mvn package -DskipTests # Build native image echo "🦭 Building Native Image..." mvn package -Pnative -DskipTests # Check file size if [ -f "target/helidon-native-app" ]; then SIZE=$(du -h target/helidon-native-app | cut -f1) echo "✅ Native image built: target/helidon-native-app (${SIZE})" # Display startup time echo "⚡ Testing startup time..." time target/helidon-native-app & PID=$! sleep 2 kill $PID 2>/dev/null else echo "❌ Native image build failed" exit 1 fi Performance Testing and Comparison
Benchmarking Script:
package com.example.benchmark; import java.net.HttpURLConnection; import java.net.URL; import java.io.BufferedReader; import java.io.InputStreamReader; public class NativeBenchmark { public static void main(String[] args) throws Exception { String baseUrl = "http://localhost:8080"; // Test startup time long startTime = System.currentTimeMillis(); // Test endpoints testEndpoint(baseUrl + "/"); testEndpoint(baseUrl + "/health"); testEndpoint(baseUrl + "/api/info"); long totalTime = System.currentTimeMillis() - startTime; System.out.println("================================="); System.out.println("🏁 Benchmark Results"); System.out.println("================================="); System.out.printf("Total response time: %d ms%n", totalTime); // Memory usage Runtime runtime = Runtime.getRuntime(); long usedMemory = runtime.totalMemory() - runtime.freeMemory(); System.out.printf("Memory usage: %.2f MB%n", usedMemory / (1024.0 * 1024.0)); } private static void testEndpoint(String urlString) { try { long start = System.currentTimeMillis(); URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(1000); conn.setReadTimeout(1000); int responseCode = conn.getResponseCode(); long duration = System.currentTimeMillis() - start; if (responseCode == 200) { try (BufferedReader in = new BufferedReader( new InputStreamReader(conn.getInputStream()))) { String response = in.readLine(); System.out.printf("✅ %s - %d ms - %s%n", urlString, duration, response); } } else { System.out.printf("❌ %s - %d ms - HTTP %d%n", urlString, duration, responseCode); } } catch (Exception e) { System.out.printf("💥 %s - Error: %s%n", urlString, e.getMessage()); } } } Performance Comparison Results:
# JVM Startup (OpenJDK 17) $ time java -jar target/helidon-native-demo-1.0.0.jar 🚀 Server started on http://localhost:8080 real 0m1.234s user 0m2.567s sys 0m0.123s # Native Image Startup $ time ./target/helidon-native-app 🚀 Server started on http://localhost:8080 real 0m0.023s user 0m0.012s sys 0m0.008s # Memory Comparison JVM: ~250MB heap + ~50MB metaspace = ~300MB Native: ~25MB total = 12x improvement
Troubleshooting Common Issues
Common Native Image Problems:
package com.example.troubleshooting; public class NativeImageTips { // 1. Reflection issues - use configuration files public static class ReflectiveClass { private String data; // This will need reflection configuration public ReflectiveClass() {} public String getData() { return data; } public void setData(String data) { this.data = data; } } // 2. Dynamic class loading - avoid or configure public void avoidDynamicLoading() { // ❌ Problematic for native image // Class.forName("com.example.DynamicClass"); // ✅ Use static initialization // Pre-initialize known classes } // 3. Resource loading - use proper patterns public void loadResourcesSafely() { // ✅ Works with native image InputStream stream = getClass().getResourceAsStream("/config.properties"); // ❌ May not work // Files.walk(Paths.get("."))... } // 4. Serialization - configure properly public static class SerializableClass implements java.io.Serializable { private static final long serialVersionUID = 1L; private String value; // Needs serialization configuration } } Debug Build Configuration:
<!-- Add to native-maven-plugin configuration --> <buildArgs> <buildArg>--verbose</buildArg> <buildArg>-H:+ReportExceptionStackTraces</buildArg> <buildArg>-H:+PrintClassInitialization</buildArg> <buildArg>-H:+TraceClassInitialization</buildArg> <buildArg>-H:Dump=:2</buildArg> <buildArg>-H:PrintAnalysisCallTree=2</buildArg> </buildArgs>
Production Deployment
Kubernetes Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: helidon-native-app labels: app: helidon-native spec: replicas: 3 selector: matchLabels: app: helidon-native template: metadata: labels: app: helidon-native spec: containers: - name: helidon-native image: my-registry/helidon-native-app:1.0.0 ports: - containerPort: 8080 resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "200m" livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 --- apiVersion: v1 kind: Service metadata: name: helidon-native-service spec: selector: app: helidon-native ports: - port: 80 targetPort: 8080 type: ClusterIP
Conclusion
Helidon SE with GraalVM Native Image provides exceptional benefits for cloud-native applications:
Key Advantages:
- Blazing Fast Startup: 20-50ms vs 1-3 seconds
- Tiny Memory Footprint: 20-50MB vs 200-300MB
- Small Container Images: ~50MB vs ~300MB
- Instant Scaling: Perfect for serverless and Kubernetes
Best Practices:
- Use explicit configuration over reflection
- Pre-initialize classes at build time
- Configure resources and serialization properly
- Implement health checks for orchestration
- Use multi-stage Docker builds
Use Cases:
- ✅ Microservices and APIs
- ✅ Serverless functions
- ✅ CLI tools and utilities
- ✅ Resource-constrained environments
- ✅ High-density deployments
By following this guide, you can build ultra-efficient Helidon SE applications that start instantly and use minimal resources, making them ideal for modern cloud-native architectures.