Serverless Java: Building Cloud Functions with Java 21

Article

Java 21 brings significant improvements that make it an excellent choice for serverless functions, with features like virtual threads, pattern matching, and enhanced performance. Cloud Functions provide a scalable, event-driven compute platform where you can run your code without managing servers. This article explores how to build, deploy, and optimize Cloud Functions using Java 21 on major cloud platforms.


Why Java 21 for Cloud Functions?

Java 21 Features Beneficial for Serverless:

  • Virtual Threads: Handle massive concurrency with lightweight threads
  • Pattern Matching: Write cleaner, more expressive code
  • Record Patterns: Simplify data processing
  • Sequenced Collections: Better collection APIs
  • String Templates: Improved string manipulation
  • Enhanced Startup: Faster cold starts with CDS archives

Google Cloud Functions with Java 21

1. Project Setup and Dependencies

<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<properties>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<functions.framework.version>1.1.0</functions.framework.version>
<gcp.libraries.version>26.32.0</gcp.libraries.version>
</properties>
<dependencies>
<!-- Google Cloud Functions -->
<dependency>
<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-api</artifactId>
<version>${functions.framework.version}</version>
<scope>provided</scope>
</dependency>
<!-- HTTP Server for local testing -->
<dependency>
<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-invoker</artifactId>
<version>${functions.framework.version}</version>
<scope>test</scope>
</dependency>
<!-- Cloud Storage -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
<version>${gcp.libraries.version}</version>
</dependency>
<!-- BigQuery -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-bigquery</artifactId>
<version>${gcp.libraries.version}</version>
</dependency>
<!-- Pub/Sub -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-pubsub</artifactId>
<version>${gcp.libraries.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-logging</artifactId>
<version>${gcp.libraries.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.functions</groupId>
<artifactId>function-maven-plugin</artifactId>
<version>0.10.2</version>
<configuration>
<functionTarget>com.example.CloudFunctionEntry</functionTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

2. HTTP Cloud Functions

// Simple HTTP Function with Java 21 features
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
public class HttpCloudFunction implements HttpFunction {
private static final Logger logger = Logger.getLogger(HttpCloudFunction.class.getName());
private static final ObjectMapper objectMapper = new ObjectMapper();
// Virtual thread executor for better concurrency
private final var virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
var context = new RequestContext(request, response);
try (var executor = virtualThreadExecutor) {
// Handle request with virtual threads
var future = executor.submit(() -> handleRequest(context));
future.get(); // Wait for completion
} catch (Exception e) {
handleError(context, e);
}
}
private Void handleRequest(RequestContext context) throws IOException {
var request = context.request();
var response = context.response();
// Set response headers
response.appendHeader("Content-Type", "application/json");
response.appendHeader("X-Java-Version", "21");
// Route requests based on method and path
var result = switch (request.getMethod()) {
case "GET" -> handleGetRequest(request);
case "POST" -> handlePostRequest(request);
case "PUT" -> handlePutRequest(request);
case "DELETE" -> handleDeleteRequest(request);
default -> new ErrorResponse("Method not allowed", 405);
};
// Write response
if (result instanceof ErrorResponse error) {
response.setStatusCode(error.statusCode());
response.getWriter().write(objectMapper.writeValueAsString(error));
} else {
response.getWriter().write(objectMapper.writeValueAsString(result));
}
return null;
}
private Object handleGetRequest(HttpRequest request) {
var path = request.getPath();
return switch (path) {
case "/health" -> new HealthResponse("healthy", System.currentTimeMillis());
case "/info" -> new InfoResponse("Java 21 Cloud Function", "1.0.0");
case "/users" -> {
var userId = request.getFirstQueryParameter("userId").orElse(null);
yield getUser(userId);
}
default -> new ErrorResponse("Not found", 404);
};
}
private Object handlePostRequest(HttpRequest request) throws IOException {
var path = request.getPath();
var body = request.getReader().lines().reduce("", String::concat);
return switch (path) {
case "/users" -> createUser(objectMapper.readValue(body, User.class));
case "/process" -> processData(objectMapper.readValue(body, ProcessRequest.class));
default -> new ErrorResponse("Not found", 404);
};
}
// Java 21 Record classes for data transfer
private record User(String id, String name, String email) {}
private record HealthResponse(String status, long timestamp) {}
private record InfoResponse(String name, String version) {}
private record ErrorResponse(String message, int statusCode) {}
private record ProcessRequest(String data, String operation) {}
private record ProcessResult(String result, long processingTime) {}
private record RequestContext(HttpRequest request, HttpResponse response) {}
// Business logic methods
private Object getUser(String userId) {
if (userId == null) {
return List.of(
new User("1", "John Doe", "[email protected]"),
new User("2", "Jane Smith", "[email protected]")
);
}
return new User(userId, "User " + userId, "user" + userId + "@example.com");
}
private Object createUser(User user) {
// Simulate user creation
var newUser = new User(
UUID.randomUUID().toString(),
user.name(),
user.email()
);
logger.info("Created user: " + newUser.id());
return newUser;
}
private Object processData(ProcessRequest request) {
var startTime = System.currentTimeMillis();
// Simulate data processing with virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = List.of(
executor.submit(() -> processChunk(request.data(), "operation1")),
executor.submit(() -> processChunk(request.data(), "operation2")),
executor.submit(() -> processChunk(request.data(), "operation3"))
);
var results = futures.stream()
.map(future -> {
try {
return future.get();
} catch (Exception e) {
return "error";
}
})
.toList();
var processingTime = System.currentTimeMillis() - startTime;
return new ProcessResult(String.join(",", results), processingTime);
}
}
private String processChunk(String data, String operation) {
// Simulate processing
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return operation + "-processed";
}
private void handleError(RequestContext context, Exception e) {
try {
var response = context.response();
response.setStatusCode(500);
response.getWriter().write(
objectMapper.writeValueAsString(
new ErrorResponse("Internal server error: " + e.getMessage(), 500)
)
);
logger.severe("Error processing request: " + e.getMessage());
} catch (IOException ioException) {
logger.severe("Failed to send error response: " + ioException.getMessage());
}
}
}

3. Background Cloud Functions (Pub/Sub)

import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.PubsubMessage;
import com.google.pubsub.v1.TopicName;
import java.util.concurrent.CompletableFuture;
public class PubSubCloudFunction implements BackgroundFunction<PubSubEvent> {
private static final Logger logger = Logger.getLogger(PubSubCloudFunction.class.getName());
private static final ObjectMapper objectMapper = new ObjectMapper();
// Virtual thread executor for async operations
private final var virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
@Override
public void accept(PubSubEvent event, Context context) {
logger.info("Processing Pub/Sub event: " + context.eventId());
try (var executor = virtualThreadExecutor) {
// Process message with virtual threads
var processingFuture = executor.submit(() -> processMessage(event, context));
// Handle result asynchronously
processingFuture.thenAccept(result -> {
if (result instanceof ProcessingResult success) {
logger.info("Successfully processed message: " + success.messageId());
// Optionally publish result to another topic
publishResult(success).thenAccept(messageId -> 
logger.info("Published result: " + messageId)
);
} else if (result instanceof ProcessingError error) {
logger.severe("Failed to process message: " + error.errorMessage());
// Handle error (e.g., publish to dead-letter topic)
handleError(event, error);
}
});
}
}
private Object processMessage(PubSubEvent event, Context context) {
try {
var data = event.data();
var message = objectMapper.readValue(
new String(java.util.Base64.getDecoder().decode(data)), 
Message.class
);
// Pattern matching with Java 21
return switch (message) {
case UserMessage userMsg -> processUserMessage(userMsg, context);
case OrderMessage orderMsg -> processOrderMessage(orderMsg, context);
case NotificationMessage notifMsg -> processNotificationMessage(notifMsg, context);
case null, default -> new ProcessingError("Unknown message type", context.eventId());
};
} catch (Exception e) {
return new ProcessingError("Processing failed: " + e.getMessage(), context.eventId());
}
}
private ProcessingResult processUserMessage(UserMessage message, Context context) {
logger.info("Processing user message: " + message.userId());
// Simulate processing
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new ProcessingResult(context.eventId(), "User processed: " + message.userId());
}
private ProcessingResult processOrderMessage(OrderMessage message, Context context) {
logger.info("Processing order: " + message.orderId());
// Use sequenced collections from Java 21
var processingSteps = List.of("validated", "processed", "completed");
var results = new LinkedHashSet<String>();
for (String step : processingSteps.reversed()) { // Java 21 reversed view
results.add(executeProcessingStep(message, step));
}
return new ProcessingResult(context.eventId(), 
"Order processed with steps: " + String.join(",", results));
}
private String executeProcessingStep(OrderMessage message, String step) {
try {
Thread.sleep(100);
return step + "-done";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return step + "-interrupted";
}
}
private ProcessingResult processNotificationMessage(NotificationMessage message, Context context) {
// String templates (Java 21 preview)
var notification = STR."""
Notification Type: \{message.type()}
Recipient: \{message.recipient()}
Content: \{message.content()}
Timestamp: \{Instant.now()}
""";
logger.info("Processed notification:\n" + notification);
return new ProcessingResult(context.eventId(), "Notification sent");
}
private CompletableFuture<String> publishResult(ProcessingResult result) {
return CompletableFuture.supplyAsync(() -> {
try {
var projectId = System.getenv("GOOGLE_CLOUD_PROJECT");
var topicId = "processing-results";
var topicName = TopicName.of(projectId, topicId);
var publisher = Publisher.newBuilder(topicName).build();
var message = PubsubMessage.newBuilder()
.setData(ByteString.copyFromUtf8(
objectMapper.writeValueAsString(result)
))
.build();
var messageId = publisher.publish(message).get();
publisher.shutdown();
return messageId;
} catch (Exception e) {
logger.severe("Failed to publish result: " + e.getMessage());
return "error";
}
}, virtualThreadExecutor);
}
private void handleError(PubSubEvent event, ProcessingError error) {
// Publish to dead-letter topic or log for manual handling
logger.severe("Error handling required for event: " + error.messageId());
}
// Record definitions for event processing
public record PubSubEvent(String data, Map<String, String> attributes) {}
public sealed interface Message permits UserMessage, OrderMessage, NotificationMessage {}
public record UserMessage(String userId, String action, Map<String, Object> data) implements Message {}
public record OrderMessage(String orderId, double amount, List<String> items) implements Message {}
public record NotificationMessage(String type, String recipient, String content) implements Message {}
public record ProcessingResult(String messageId, String result) {}
public record ProcessingError(String errorMessage, String messageId) {}
}

4. Storage Trigger Function

import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.util.concurrent.StructuredTaskScope;
public class StorageCloudFunction implements BackgroundFunction<StorageEvent> {
private static final Logger logger = Logger.getLogger(StorageCloudFunction.class.getName());
private final Storage storage = StorageOptions.getDefaultInstance().getService();
@Override
public void accept(StorageEvent event, Context context) {
logger.info("Processing storage event for: " + event.name());
// Use Structured Concurrency from Java 21
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Process file with multiple concurrent operations
var metadataTask = scope.fork(() -> extractMetadata(event));
var contentTask = scope.fork(() -> processContent(event));
var validationTask = scope.fork(() -> validateFile(event));
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Propagate exceptions
// Combine results
var metadata = metadataTask.get();
var content = contentTask.get();
var validation = validationTask.get();
logger.info(String.format("""
File processed successfully:
- Metadata: %s
- Content size: %d
- Validation: %s
""", metadata, content.length(), validation));
} catch (Exception e) {
logger.severe("Failed to process storage event: " + e.getMessage());
}
}
private FileMetadata extractMetadata(StorageEvent event) throws Exception {
var blob = getBlob(event);
return new FileMetadata(
event.name(),
blob.getSize(),
blob.getUpdateTime(),
blob.getContentType(),
extractFileExtension(event.name())
);
}
private String processContent(StorageEvent event) throws Exception {
var blob = getBlob(event);
var content = new String(blob.getContent());
// Use pattern matching for switch expressions
return switch (getFileExtension(event.name())) {
case "json" -> processJsonContent(content);
case "csv" -> processCsvContent(content);
case "txt" -> processTextContent(content);
case null, default -> "unsupported-format";
};
}
private String validateFile(StorageEvent event) throws Exception {
var blob = getBlob(event);
// Virtual threads for I/O operations
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var sizeCheck = executor.submit(() -> validateSize(blob.getSize()));
var typeCheck = executor.submit(() -> validateType(blob.getContentType()));
var nameCheck = executor.submit(() -> validateName(event.name()));
return String.format("size:%s,type:%s,name:%s", 
sizeCheck.get(), typeCheck.get(), nameCheck.get());
}
}
private Blob getBlob(StorageEvent event) {
return storage.get(event.bucket(), event.name());
}
private String getFileExtension(String filename) {
return Optional.ofNullable(filename)
.filter(f -> f.contains("."))
.map(f -> f.substring(filename.lastIndexOf(".") + 1))
.orElse(null);
}
// Processing methods
private String processJsonContent(String content) {
try {
var jsonNode = objectMapper.readTree(content);
return "json-validated-size:" + jsonNode.size();
} catch (Exception e) {
return "json-invalid";
}
}
private String processCsvContent(String content) {
var lines = content.lines().count();
return "csv-lines:" + lines;
}
private String processTextContent(String content) {
var wordCount = content.split("\\s+").length;
return "text-words:" + wordCount;
}
private String validateSize(long size) {
return size > 0 && size < 10_000_000 ? "valid" : "invalid";
}
private String validateType(String contentType) {
return switch (contentType) {
case "application/json", "text/csv", "text/plain" -> "valid";
case null, default -> "invalid";
};
}
private String validateName(String filename) {
return filename != null && !filename.contains("..") ? "valid" : "invalid";
}
private String extractFileExtension(String filename) {
return Optional.ofNullable(filename)
.filter(f -> f.contains("."))
.map(f -> f.substring(filename.lastIndexOf(".") + 1))
.orElse("unknown");
}
// Record definitions
public record StorageEvent(String bucket, String name, String metageneration) {}
public record FileMetadata(String name, long size, Long updateTime, 
String contentType, String extension) {}
}

5. Deployment Configuration

<!-- function.yaml for Google Cloud Functions -->
apiVersion: v1
kind: ConfigMap
metadata:
name: java-function-config
data:
function.yaml: |
# Function configuration for Java 21
name: java21-http-function
runtime: java21
entryPoint: com.example.HttpCloudFunction
availableMemory: 512Mi
maxInstances: 100
minInstances: 0
timeout: 60s
environmentVariables:
JAVA_TOOL_OPTIONS: >
-XX:+UseZGC
-Xmx256m
-Xms128m
-XX:+UseContainerSupport
-Djava.util.logging.SimpleFormatter.format="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n"
labels:
java-version: "21"
environment: "production"

6. AWS Lambda with Java 21

// For AWS Lambda (using custom runtime)
public class AwsLambdaFunction 
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final var virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
var logger = context.getLogger();
try (var executor = virtualThreadExecutor) {
return executor.submit(() -> processRequest(input, context)).get();
} catch (Exception e) {
logger.log("Error: " + e.getMessage());
return createErrorResponse(500, "Internal server error");
}
}
private APIGatewayProxyResponseEvent processRequest(APIGatewayProxyRequestEvent input, Context context) {
var path = input.getPath();
var httpMethod = input.getHttpMethod();
logger.info("Processing " + httpMethod + " " + path);
var response = switch (httpMethod) {
case "GET" -> handleGet(path, input.getQueryStringParameters());
case "POST" -> handlePost(path, input.getBody());
default -> createErrorResponse(405, "Method not allowed");
};
return response;
}
private APIGatewayProxyResponseEvent handleGet(String path, Map<String, String> queryParams) {
return switch (path) {
case "/health" -> createSuccessResponse(new HealthCheck("healthy", Instant.now()));
case "/users" -> {
var userId = queryParams != null ? queryParams.get("userId") : null;
yield createSuccessResponse(getUser(userId));
}
default -> createErrorResponse(404, "Not found");
};
}
private APIGatewayProxyResponseEvent handlePost(String path, String body) {
try {
return switch (path) {
case "/users" -> {
var user = objectMapper.readValue(body, User.class);
yield createSuccessResponse(createUser(user));
}
case "/process" -> {
var request = objectMapper.readValue(body, ProcessRequest.class);
yield createSuccessResponse(processData(request));
}
default -> createErrorResponse(404, "Not found");
};
} catch (Exception e) {
return createErrorResponse(400, "Invalid request body");
}
}
private APIGatewayProxyResponseEvent createSuccessResponse(Object body) {
try {
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(objectMapper.writeValueAsString(body))
.withHeaders(Map.of("Content-Type", "application/json"));
} catch (Exception e) {
return createErrorResponse(500, "Failed to serialize response");
}
}
private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(statusCode)
.withBody("{\"error\":\"" + message + "\"}")
.withHeaders(Map.of("Content-Type", "application/json"));
}
// Same record definitions as previous examples
record HealthCheck(String status, Instant timestamp) {}
record User(String id, String name, String email) {}
record ProcessRequest(String data, String operation) {}
private Object getUser(String userId) { /* implementation */ }
private Object createUser(User user) { /* implementation */ }
private Object processData(ProcessRequest request) { /* implementation */ }
}

7. Build and Deployment Scripts

#!/bin/bash
# deploy-gcp.sh
set -e
PROJECT_ID="your-project-id"
FUNCTION_NAME="java21-http-function"
REGION="us-central1"
JAR_FILE="target/cloud-functions-1.0.0.jar"
echo "Building Java 21 Cloud Function..."
mvn clean package -DskipTests
echo "Deploying to Google Cloud Functions..."
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--runtime=java21 \
--region=$REGION \
--source=target/deployment \
--entry-point=com.example.HttpCloudFunction \
--trigger-http \
--allow-unauthenticated \
--memory=512Mi \
--timeout=60s \
--min-instances=0 \
--max-instances=100 \
--set-env-vars=JAVA_TOOL_OPTIONS="-XX:+UseZGC -Xmx256m -Xms128m" \
--project=$PROJECT_ID
echo "Deployment completed successfully!"

Best Practices for Java 21 Cloud Functions

1. Cold Start Optimization

  • Use Class Data Sharing (CDS) archives
  • Keep deployment packages small
  • Use minimal dependencies
  • Enable minimum instances for critical functions

2. Memory Management

  • Configure appropriate memory settings
  • Use ZGC for better pause times
  • Monitor memory usage with cloud monitoring

3. Concurrency Patterns

  • Leverage virtual threads for I/O operations
  • Use structured concurrency for complex workflows
  • Implement proper error handling in async code

4. Monitoring and Logging

  • Implement comprehensive logging
  • Use cloud-native monitoring tools
  • Set up alerts for errors and performance issues

Performance Comparison

FeatureJava 11Java 21Improvement
Cold Start~1500ms~800ms~47% faster
Memory Usage256MB180MB~30% reduction
Concurrent Requests100010,000+10x with virtual threads
Deployment Size45MB38MB~15% smaller

Conclusion

Java 21 brings revolutionary improvements to cloud functions, making it a compelling choice for serverless applications. Key benefits include:

  • Faster cold starts with CDS and optimized runtime
  • Massive concurrency with virtual threads
  • Reduced memory footprint with better GC algorithms
  • Cleaner code with records and pattern matching
  • Enhanced performance with modern JVM features

By leveraging Java 21's advanced features, you can build highly scalable, efficient, and maintainable cloud functions that outperform previous Java versions while providing better developer experience and operational efficiency.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper