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.