google-site-verification: google61fe8ba583a51912.html
Modern Serverless Java: AWS Lambda Java 21 Runtime

AWS Lambda's Java 21 runtime provides the latest LTS version of Java for serverless applications, offering improved performance, new language features, and enhanced developer productivity. This runtime leverages Amazon Corretto 21, a no-cost, multi-platform, production-ready distribution of OpenJDK.


What's New in Java 21 for Lambda?

Java 21 introduces powerful features for serverless development:

  • Virtual Threads (Project Loom) for massive concurrency
  • Record Patterns and Pattern Matching for cleaner code
  • Sequenced Collections for better collection APIs
  • String Templates (preview) for simplified string composition
  • Structured Concurrency for easier concurrent programming

Lambda Java 21 Architecture

[Lambda Function] → [Java 21 Runtime] → [AWS Services] → [External Systems] | | | | Business logic Corretto 21 S3, DynamoDB, APIs, Databases execution SQS, EventBridge Message queues

Hands-On Tutorial: Building Modern Lambda Functions with Java 21

Let's build a complete serverless application using Java 21 features and modern AWS services.

Step 1: Project Setup and Dependencies

Maven Dependencies (pom.xml):

<?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>lambda-java21</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- AWS Lambda --> <aws.lambda.java.core.version>1.2.3</aws.lambda.java.core.version> <aws.java.sdk.version>2.21.0</aws.java.sdk.version> <!-- Logging --> <slf4j.version>2.0.9</slf4j.version> <!-- JSON Processing --> <jackson.version>2.16.1</jackson.version> <!-- Testing --> <junit.version>5.10.1</junit.version> <testcontainers.version>1.19.3</testcontainers.version> </properties> <dependencies> <!-- AWS Lambda Java Core --> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>${aws.lambda.java.core.version}</version> </dependency> <!-- AWS Lambda Java Events --> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-events</artifactId> <version>3.11.3</version> </dependency> <!-- AWS SDK v2 --> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>bom</artifactId> <version>${aws.java.sdk.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>dynamodb</artifactId> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>sqs</artifactId> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>secretsmanager</artifactId> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>lambda</artifactId> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${jackson.version}</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> <!-- Utilities --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.13.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.13.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>localstack</artifactId> <version>${testcontainers.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.8.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility</artifactId> <version>4.2.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <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> <arg>--enable-preview</arg> </compilerArgs> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>

Step 2: Base Lambda Handler with Java 21 Features

src/main/java/com/example/lambda/core/BaseLambdaHandler.java:

package com.example.lambda.core; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.StructuredTaskScope; /** * Base Lambda handler with Java 21 features and common functionality */ @Slf4j public abstract class BaseLambdaHandler<I, O> implements RequestHandler<I, O> { protected final ObjectMapper objectMapper; protected BaseLambdaHandler() { this.objectMapper = new ObjectMapper(); this.objectMapper.findAndRegisterModules(); } @Override public O handleRequest(I input, Context context) { log.info("Lambda invocation started - Function: {}, Version: {}, Memory: {}MB", context.getFunctionName(), context.getFunctionVersion(), context.getMemoryLimitInMB()); try { // Log input (be careful with sensitive data) logInput(input); // Process the request O result = processRequest(input, context); log.info("Lambda invocation completed successfully"); return result; } catch (Exception e) { log.error("Lambda invocation failed", e); throw new LambdaExecutionException("Request processing failed", e); } } /** * Main processing method to be implemented by subclasses */ protected abstract O processRequest(I input, Context context); /** * Process request asynchronously using virtual threads */ protected CompletableFuture<O> processAsync(I input, Context context) { return CompletableFuture.supplyAsync(() -> processRequest(input, context), Executors.newVirtualThreadPerTaskExecutor()); } /** * Execute multiple tasks concurrently using Structured Concurrency */ protected <T, U> ConcurrentResult<T, U> executeConcurrently(Task<T> task1, Task<U> task2) { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var fork1 = scope.fork(task1::execute); var fork2 = scope.fork(task2::execute); scope.join(); // Join both forks scope.throwIfFailed(); // Propagate exception if any failed return new ConcurrentResult<>( fork1.get(), fork2.get() ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new LambdaExecutionException("Concurrent execution interrupted", e); } } /** * Log input safely (mask sensitive fields) */ protected void logInput(I input) { if (log.isDebugEnabled()) { try { String inputJson = objectMapper.writeValueAsString(input); String maskedInput = maskSensitiveData(inputJson); log.debug("Lambda input: {}", maskedInput); } catch (JsonProcessingException e) { log.debug("Failed to serialize input for logging"); } } } /** * Mask sensitive data in logs */ protected String maskSensitiveData(String json) { // Implement sensitive data masking return json.replaceAll("(\"password\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3") .replaceAll("(\"apiKey\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3") .replaceAll("(\"token\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3"); } /** * Create standardized error response */ protected Map<String, Object> createErrorResponse(String message, String errorCode) { return Map.of( "error", true, "message", message, "errorCode", errorCode, "timestamp", System.currentTimeMillis() ); } /** * Create standardized success response */ protected Map<String, Object> createSuccessResponse(Object data, String message) { return Map.of( "success", true, "data", data, "message", message, "timestamp", System.currentTimeMillis() ); } // Java 21 Record for concurrent results public record ConcurrentResult<T, U>(T result1, U result2) {} // Functional interface for tasks @FunctionalInterface public interface Task<T> { T execute() throws Exception; } public static class LambdaExecutionException extends RuntimeException { public LambdaExecutionException(String message) { super(message); } public LambdaExecutionException(String message, Throwable cause) { super(message, cause); } } }

Step 3: Virtual Threads for High-Concurrency Processing

src/main/java/com/example/lambda/handler/ImageProcessingHandler.java:

package com.example.lambda.handler; import com.example.lambda.core.BaseLambdaHandler; import com.amazonaws.services.lambda.runtime.Context; import lombok.extern.slf4j.Slf4j; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.net.URI; import java.util.List; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; /** * Image processing handler using Java 21 virtual threads for high concurrency */ @Slf4j public class ImageProcessingHandler extends BaseLambdaHandler<ImageProcessingHandler.ImageRequest, ImageProcessingHandler.ProcessResult> { private final S3Client s3Client; public ImageProcessingHandler() { this.s3Client = S3Client.create(); } // Constructor for testing public ImageProcessingHandler(S3Client s3Client) { this.s3Client = s3Client; } @Override protected ProcessResult processRequest(ImageRequest request, Context context) { log.info("Processing image from bucket: {}, key: {}", request.sourceBucket(), request.sourceKey()); try { // Download image from S3 using virtual threads byte[] originalImage = downloadImageAsync(request.sourceBucket(), request.sourceKey()).get(); // Process multiple transformations concurrently using virtual threads List<CompletableFuture<ProcessedImage>> transformations = processTransformationsAsync( originalImage, request.transformations()); // Wait for all transformations to complete CompletableFuture.allOf(transformations.toArray(new CompletableFuture[0])).join(); // Collect results List<ProcessedImage> results = transformations.stream() .map(CompletableFuture::join) .toList(); // Upload results to S3 uploadResultsAsync(results, request.destinationBucket()).join(); return new ProcessResult( true, "Successfully processed " + results.size() + " transformations", results.stream() .map(ProcessedImage::toSummary) .toList() ); } catch (Exception e) { log.error("Image processing failed", e); return new ProcessResult(false, "Processing failed: " + e.getMessage(), List.of()); } } /** * Download image from S3 using virtual threads */ private CompletableFuture<byte[]> downloadImageAsync(String bucket, String key) { return CompletableFuture.supplyAsync(() -> { try { log.debug("Downloading image from S3: {}/{}", bucket, key); var getObjectRequest = GetObjectRequest.builder() .bucket(bucket) .key(key) .build(); return s3Client.getObjectAsBytes(getObjectRequest).asByteArray(); } catch (Exception e) { throw new RuntimeException("Failed to download image from S3", e); } }, Executors.newVirtualThreadPerTaskExecutor()); } /** * Process multiple image transformations concurrently using virtual threads */ private List<CompletableFuture<ProcessedImage>> processTransformationsAsync( byte[] originalImage, List<Transformation> transformations) { return transformations.stream() .map(transformation -> processTransformationAsync(originalImage, transformation)) .toList(); } /** * Process single transformation using virtual threads */ private CompletableFuture<ProcessedImage> processTransformationAsync( byte[] originalImage, Transformation transformation) { return CompletableFuture.supplyAsync(() -> { try { log.debug("Processing transformation: {}", transformation.type()); BufferedImage image = ImageIO.read(new ByteArrayInputStream(originalImage)); if (image == null) { throw new IllegalArgumentException("Invalid image data"); } BufferedImage processedImage = switch (transformation.type()) { case "thumbnail" -> createThumbnail(image, transformation); case "resize" -> resizeImage(image, transformation); case "grayscale" -> convertToGrayscale(image); case "rotate" -> rotateImage(image, transformation); default -> throw new IllegalArgumentException("Unsupported transformation: " + transformation.type()); }; // Convert to desired format String format = transformation.parameters().getOrDefault("format", "jpg"); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(processedImage, format, outputStream); return new ProcessedImage( transformation.type(), format, outputStream.toByteArray(), processedImage.getWidth(), processedImage.getHeight() ); } catch (Exception e) { throw new RuntimeException("Transformation failed: " + transformation.type(), e); } }, Executors.newVirtualThreadPerTaskExecutor()); } /** * Upload processed images to S3 using virtual threads */ private CompletableFuture<Void> uploadResultsAsync(List<ProcessedImage> results, String bucket) { List<CompletableFuture<Void>> uploads = results.stream() .map(result -> uploadImageAsync(result, bucket)) .toList(); return CompletableFuture.allOf(uploads.toArray(new CompletableFuture[0])); } private CompletableFuture<Void> uploadImageAsync(ProcessedImage image, String bucket) { return CompletableFuture.runAsync(() -> { try { String key = "processed/" + image.type() + "-" + System.currentTimeMillis() + "." + image.format(); var putObjectRequest = PutObjectRequest.builder() .bucket(bucket) .key(key) .contentType("image/" + image.format()) .build(); s3Client.putObject(putObjectRequest, RequestBody.fromBytes(image.data())); log.debug("Uploaded processed image to S3: {}/{}", bucket, key); } catch (Exception e) { throw new RuntimeException("Failed to upload image to S3", e); } }, Executors.newVirtualThreadPerTaskExecutor()); } // Image processing methods private BufferedImage createThumbnail(BufferedImage original, Transformation transformation) { int width = Integer.parseInt(transformation.parameters().getOrDefault("width", "200")); int height = Integer.parseInt(transformation.parameters().getOrDefault("height", "200")); BufferedImage thumbnail = new BufferedImage(width, height, original.getType()); Graphics2D g = thumbnail.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(original, 0, 0, width, height, null); g.dispose(); return thumbnail; } private BufferedImage resizeImage(BufferedImage original, Transformation transformation) { int newWidth = Integer.parseInt(transformation.parameters().get("width")); int newHeight = Integer.parseInt(transformation.parameters().get("height")); BufferedImage resized = new BufferedImage(newWidth, newHeight, original.getType()); Graphics2D g = resized.createGraphics(); g.drawImage(original, 0, 0, newWidth, newHeight, null); g.dispose(); return resized; } private BufferedImage convertToGrayscale(BufferedImage original) { BufferedImage grayscale = new BufferedImage( original.getWidth(), original.getHeight(), BufferedImage.TYPE_BYTE_GRAY); Graphics2D g = grayscale.createGraphics(); g.drawImage(original, 0, 0, null); g.dispose(); return grayscale; } private BufferedImage rotateImage(BufferedImage original, Transformation transformation) { double angle = Math.toRadians(Double.parseDouble( transformation.parameters().getOrDefault("angle", "90"))); double sin = Math.abs(Math.sin(angle)); double cos = Math.abs(Math.cos(angle)); int newWidth = (int) Math.floor(original.getWidth() * cos + original.getHeight() * sin); int newHeight = (int) Math.floor(original.getHeight() * cos + original.getWidth() * sin); BufferedImage rotated = new BufferedImage(newWidth, newHeight, original.getType()); Graphics2D g = rotated.createGraphics(); g.translate((newWidth - original.getWidth()) / 2, (newHeight - original.getHeight()) / 2); g.rotate(angle, original.getWidth() / 2.0, original.getHeight() / 2.0); g.drawRenderedImage(original, null); g.dispose(); return rotated; } // Java 21 Records for data transfer public record ImageRequest( String sourceBucket, String sourceKey, String destinationBucket, List<Transformation> transformations ) {} public record Transformation( String type, Map<String, String> parameters ) {} public record ProcessedImage( String type, String format, byte[] data, int width, int height ) { public Map<String, Object> toSummary() { return Map.of( "type", type, "format", format, "width", width, "height", height, "size", data.length ); } } public record ProcessResult( boolean success, String message, List<Map<String, Object>> processedImages ) {} }

Step 4: DynamoDB Integration with Structured Concurrency

src/main/java/com/example/lambda/handler/UserManagementHandler.java:

package com.example.lambda.handler; import com.example.lambda.core.BaseLambdaHandler; import com.amazonaws.services.lambda.runtime.Context; import lombok.extern.slf4j.Slf4j; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.StructuredTaskScope; /** * User management handler using Java 21 structured concurrency for database operations */ @Slf4j public class UserManagementHandler extends BaseLambdaHandler<Map<String, Object>, Map<String, Object>> { private final DynamoDbEnhancedClient enhancedClient; private final DynamoDbTable<User> userTable; private static final String TABLE_NAME = System.getenv("USERS_TABLE"); public UserManagementHandler() { DynamoDbClient dynamoDbClient = DynamoDbClient.create(); this.enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(dynamoDbClient) .build(); this.userTable = enhancedClient.table(TABLE_NAME, TableSchema.fromBean(User.class)); } // Constructor for testing public UserManagementHandler(DynamoDbEnhancedClient enhancedClient) { this.enhancedClient = enhancedClient; this.userTable = enhancedClient.table(TABLE_NAME, TableSchema.fromBean(User.class)); } @Override protected Map<String, Object> processRequest(Map<String, Object> input, Context context) { String httpMethod = (String) input.get("httpMethod"); String path = (String) input.get("path"); log.info("Processing {} request for path: {}", httpMethod, path); return switch (httpMethod) { case "POST" -> handlePostRequest(input); case "GET" -> handleGetRequest(path, input); case "PUT" -> handlePutRequest(path, input); case "DELETE" -> handleDeleteRequest(path); default -> createErrorResponse("Method not allowed", "METHOD_NOT_ALLOWED"); }; } private Map<String, Object> handlePostRequest(Map<String, Object> input) { try { @SuppressWarnings("unchecked") Map<String, Object> body = (Map<String, Object>) input.get("body"); String email = (String) body.get("email"); String name = (String) body.get("name"); // Check if user already exists User existingUser = findUserByEmail(email); if (existingUser != null) { return createErrorResponse("User already exists", "USER_EXISTS"); } // Create new user User user = new User(); user.setUserId(UUID.randomUUID().toString()); user.setEmail(email); user.setName(name); user.setCreatedAt(Instant.now().toString()); user.setUpdatedAt(Instant.now().toString()); user.setStatus("ACTIVE"); userTable.putItem(user); log.info("Created user: {}", user.getUserId()); return createSuccessResponse( Map.of( "userId", user.getUserId(), "email", user.getEmail(), "name", user.getName() ), "User created successfully" ); } catch (Exception e) { log.error("Failed to create user", e); return createErrorResponse("Failed to create user", "CREATE_FAILED"); } } private Map<String, Object> handleGetRequest(String path, Map<String, Object> input) { if ("/users".equals(path)) { return getAllUsers(input); } else if (path.startsWith("/users/")) { String userId = extractUserId(path); return getUserById(userId); } else { return createErrorResponse("Not found", "NOT_FOUND"); } } private Map<String, Object> getAllUsers(Map<String, Object> input) { try { @SuppressWarnings("unchecked") Map<String, String> queryParams = (Map<String, String>) input.get("queryStringParameters"); int limit = queryParams != null && queryParams.containsKey("limit") ? Integer.parseInt(queryParams.get("limit")) : 10; // Using structured concurrency for parallel operations try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var usersTask = scope.fork(() -> userTable.scan().items().stream() .limit(limit) .map(this::toUserResponse) .toList()); var countTask = scope.fork(() -> userTable.scan().items().stream().count()); scope.join(); scope.throwIfFailed(); List<Map<String, Object>> users = usersTask.get(); long totalCount = countTask.get(); return createSuccessResponse( Map.of( "users", users, "totalCount", totalCount, "returnedCount", users.size() ), "Users retrieved successfully" ); } } catch (Exception e) { log.error("Failed to retrieve users", e); return createErrorResponse("Failed to retrieve users", "RETRIEVE_FAILED"); } } private Map<String, Object> getUserById(String userId) { try { User user = userTable.getItem(r -> r.key(k -> k.partitionValue(userId))); if (user == null) { return createErrorResponse("User not found", "USER_NOT_FOUND"); } return createSuccessResponse(toUserResponse(user), "User retrieved successfully"); } catch (Exception e) { log.error("Failed to retrieve user: {}", userId, e); return createErrorResponse("Failed to retrieve user", "RETRIEVE_FAILED"); } } private Map<String, Object> handlePutRequest(String path, Map<String, Object> input) { String userId = extractUserId(path); try { User existingUser = userTable.getItem(r -> r.key(k -> k.partitionValue(userId))); if (existingUser == null) { return createErrorResponse("User not found", "USER_NOT_FOUND"); } @SuppressWarnings("unchecked") Map<String, Object> body = (Map<String, Object>) input.get("body"); // Update user fields if (body.containsKey("name")) { existingUser.setName((String) body.get("name")); } if (body.containsKey("status")) { existingUser.setStatus((String) body.get("status")); } existingUser.setUpdatedAt(Instant.now().toString()); userTable.putItem(existingUser); log.info("Updated user: {}", userId); return createSuccessResponse(toUserResponse(existingUser), "User updated successfully"); } catch (Exception e) { log.error("Failed to update user: {}", userId, e); return createErrorResponse("Failed to update user", "UPDATE_FAILED"); } } private Map<String, Object> handleDeleteRequest(String path) { String userId = extractUserId(path); try { User existingUser = userTable.getItem(r -> r.key(k -> k.partitionValue(userId))); if (existingUser == null) { return createErrorResponse("User not found", "USER_NOT_FOUND"); } // Soft delete by updating status existingUser.setStatus("DELETED"); existingUser.setUpdatedAt(Instant.now().toString()); userTable.putItem(existingUser); log.info("Soft deleted user: {}", userId); return createSuccessResponse( Map.of("userId", userId), "User deleted successfully" ); } catch (Exception e) { log.error("Failed to delete user: {}", userId, e); return createErrorResponse("Failed to delete user", "DELETE_FAILED"); } } private User findUserByEmail(String email) { return userTable.scan().items().stream() .filter(user -> email.equals(user.getEmail())) .findFirst() .orElse(null); } private String extractUserId(String path) { return path.substring("/users/".length()); } private Map<String, Object> toUserResponse(User user) { return Map.of( "userId", user.getUserId(), "email", user.getEmail(), "name", user.getName(), "status", user.getStatus(), "createdAt", user.getCreatedAt(), "updatedAt", user.getUpdatedAt() ); } // DynamoDB entity class public static class User { private String userId; private String email; private String name; private String status; private String createdAt; private String updatedAt; // Getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } public String getUpdatedAt() { return updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } } }

Step 5: Event-Driven Processing with Java 21 Features

src/main/java/com/example/lambda/handler/OrderProcessingHandler.java:

package com.example.lambda.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.example.lambda.core.BaseLambdaHandler; import lombok.extern.slf4j.Slf4j; import software.amazon.awssdk.services.sns.SnsClient; import software.amazon.awssdk.services.sns.model.PublishRequest; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; /** * Order processing handler using Java 21 features for event-driven processing */ @Slf4j public class OrderProcessingHandler extends BaseLambdaHandler<SQSEvent, String> { private final SnsClient snsClient; private final String notificationTopicArn; public OrderProcessingHandler() { this.snsClient = SnsClient.create(); this.notificationTopicArn = System.getenv("NOTIFICATION_TOPIC_ARN"); } // Constructor for testing public OrderProcessingHandler(SnsClient snsClient, String topicArn) { this.snsClient = snsClient; this.notificationTopicArn = topicArn; } @Override protected String processRequest(SQSEvent event, Context context) { log.info("Processing {} SQS messages", event.getRecords().size()); // Process messages concurrently using virtual threads List<CompletableFuture<Void>> processingFutures = event.getRecords().stream() .map(message -> processMessageAsync(message, context)) .toList(); // Wait for all processing to complete CompletableFuture.allOf(processingFutures.toArray(new CompletableFuture[0])).join(); log.info("Successfully processed {} messages", event.getRecords().size()); return "Processed " + event.getRecords().size() + " messages"; } private CompletableFuture<Void> processMessageAsync(SQSEvent.SQSMessage message, Context context) { return CompletableFuture.runAsync(() -> { try { log.debug("Processing message: {}", message.getMessageId()); // Parse order from message body Order order = objectMapper.readValue(message.getBody(), Order.class); // Validate order using Java 21 pattern matching if (!isValidOrder(order)) { log.warn("Invalid order received: {}", order.orderId()); return; } // Process order steps concurrently processOrderConcurrently(order).join(); log.info("Successfully processed order: {}", order.orderId()); } catch (Exception e) { log.error("Failed to process message: {}", message.getMessageId(), e); // Message will be retried or sent to DLQ based on SQS configuration } }, Executors.newVirtualThreadPerTaskExecutor()); } /** * Process order steps concurrently using structured concurrency */ private CompletableFuture<Void> processOrderConcurrently(Order order) { return CompletableFuture.runAsync(() -> { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Execute order processing steps concurrently var inventoryTask = scope.fork(() -> updateInventory(order)); var paymentTask = scope.fork(() -> processPayment(order)); var notificationTask = scope.fork(() -> sendNotification(order)); scope.join(); // Wait for all tasks scope.throwIfFailed(); // Propagate any failures // All tasks completed successfully log.debug("All order processing tasks completed for order: {}", order.orderId()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Order processing interrupted", e); } catch (Exception e) { throw new RuntimeException("Order processing failed", e); } }, Executors.newVirtualThreadPerTaskExecutor()); } private Void updateInventory(Order order) { log.debug("Updating inventory for order: {}", order.orderId()); // Simulate inventory update try { Thread.sleep(100); // Simulate database operation } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Inventory update interrupted", e); } log.debug("Inventory updated for order: {}", order.orderId()); return null; } private Void processPayment(Order order) { log.debug("Processing payment for order: {}", order.orderId()); // Simulate payment processing try { Thread.sleep(200); // Simulate payment gateway call } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Payment processing interrupted", e); } // Validate payment amount using Java 21 features if (order.amount() <= 0) { throw new RuntimeException("Invalid payment amount: " + order.amount()); } log.debug("Payment processed for order: {}", order.orderId()); return null; } private Void sendNotification(Order order) { log.debug("Sending notification for order: {}", order.orderId()); try { String message = String.format( "Order %s for %s has been processed. Amount: $%.2f", order.orderId(), order.customerName(), order.amount() ); var publishRequest = PublishRequest.builder() .topicArn(notificationTopicArn) .message(message) .subject("Order Processed") .build(); snsClient.publish(publishRequest); log.debug("Notification sent for order: {}", order.orderId()); } catch (Exception e) { log.error("Failed to send notification for order: {}", order.orderId(), e); // Don't fail the entire order processing if notification fails } return null; } /** * Validate order using Java 21 pattern matching and record patterns */ private boolean isValidOrder(Order order) { // Using Java 21 pattern matching for instanceof and record patterns return order instanceof Order(String orderId, String customerId, String customerName, Double amount, List<OrderItem> items) && orderId != null && !orderId.isBlank() && customerId != null && !customerId.isBlank() && customerName != null && !customerName.isBlank() && amount != null && amount > 0 && items != null && !items.isEmpty() && items.stream().allMatch(this::isValidOrderItem); } private boolean isValidOrderItem(OrderItem item) { return item instanceof OrderItem(String productId, Integer quantity, Double price) && productId != null && !productId.isBlank() && quantity != null && quantity > 0 && price != null && price >= 0; } // Java 21 Records for event data public record Order( String orderId, String customerId, String customerName, Double amount, List<OrderItem> items ) {} public record OrderItem( String productId, Integer quantity, Double price ) {} }

Step 6: SAM Template for Deployment

template.yml:

AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Parameters: Environment: Type: String Default: dev AllowedValues: - dev - staging - prod Globals: Function: Runtime: java21 Architectures: - x86_64 Timeout: 30 MemorySize: 512 Environment: Variables: JAVA_TOOL_OPTIONS: > -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xshare:on -XX:MaxRAMPercentage=75.0 -Xss256k POWERTOOLS_SERVICE_NAME: lambda-java21 LOG_LEVEL: INFO Resources: # Image Processing Function ImageProcessingFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub image-processor-${Environment} CodeUri: target/lambda-java21-1.0.0.jar Handler: com.example.lambda.handler.ImageProcessingHandler::handleRequest Policies: - S3ReadPolicy: BucketName: !Ref SourceImageBucket - S3WritePolicy: BucketName: !Ref ProcessedImageBucket Environment: Variables: SOURCE_BUCKET: !Ref SourceImageBucket DESTINATION_BUCKET: !Ref ProcessedImageBucket Events: ApiEvent: Type: Api Properties: Path: /images/process Method: post # User Management Function UserManagementFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub user-manager-${Environment} CodeUri: target/lambda-java21-1.0.0.jar Handler: com.example.lambda.handler.UserManagementHandler::handleRequest Policies: - DynamoDBCrudPolicy: TableName: !Ref UsersTable Environment: Variables: USERS_TABLE: !Ref UsersTable Events: UserApi: Type: Api Properties: Path: /users Method: any UserByIdApi: Type: Api Properties: Path: /users/{userId} Method: any # Order Processing Function OrderProcessingFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub order-processor-${Environment} CodeUri: target/lambda-java21-1.0.0.jar Handler: com.example.lambda.handler.OrderProcessingHandler::handleRequest Policies: - SNSPublishMessagePolicy: TopicName: !GetRef NotificationTopic - SQSPollerPolicy: QueueName: !GetRef OrderQueue Environment: Variables: NOTIFICATION_TOPIC_ARN: !Ref NotificationTopic Events: OrderQueueEvent: Type: SQS Properties: Queue: !GetAtt OrderQueue.Arn BatchSize: 10 # S3 Buckets SourceImageBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub source-images-${AWS::AccountId}-${Environment} LifecycleConfiguration: Rules: - Id: CleanupOldImages Status: Enabled ExpirationInDays: 7 ProcessedImageBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub processed-images-${AWS::AccountId}-${Environment} # DynamoDB Table UsersTable: Type: AWS::DynamoDB::Table Properties: TableName: !Sub users-${Environment} BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH SSESpecification: SSEEnabled: true # SQS Queue OrderQueue: Type: AWS::SQS::Queue Properties: QueueName: !Sub orders-${Environment} VisibilityTimeout: 300 RedrivePolicy: deadLetterTargetArn: !GetAtt OrderDLQ.Arn maxReceiveCount: 3 OrderDLQ: Type: AWS::SQS::Queue Properties: QueueName: !Sub orders-dlq-${Environment} # SNS Topic NotificationTopic: Type: AWS::SNS::Topic Properties: TopicName: !Sub notifications-${Environment} Outputs: ApiUrl: Description: "API Gateway URL" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}" ImageProcessingFunctionArn: Description: "Image Processing Function ARN" Value: !GetAtt ImageProcessingFunction.Arn UserManagementFunctionArn: Description: "User Management Function ARN" Value: !GetAtt UserManagementFunction.Arn OrderProcessingFunctionArn: Description: "Order Processing Function ARN" Value: !GetAtt OrderProcessingFunction.Arn

Step 7: Build and Deployment Scripts

build-and-deploy.sh:

#!/bin/bash set -e # Configuration STACK_NAME="lambda-java21-demo" ENVIRONMENT="${1:-dev}" REGION="${AWS_REGION:-us-east-1}" ARTIFACT_BUCKET="lambda-artifacts-$(aws sts get-caller-identity --query Account --output text)" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } build_project() { log_info "Building project with Maven..." # Clean and build mvn clean package -DskipTests # Verify build if [ ! -f "target/lambda-java21-1.0.0.jar" ]; then log_error "Build failed - JAR file not found" exit 1 fi log_info "Build completed successfully" } upload_artifacts() { log_info "Uploading artifacts to S3..." # Create artifact bucket if it doesn't exist if ! aws s3 ls "s3://$ARTIFACT_BUCKET" 2>&1 | grep -q 'NoSuchBucket'; then log_info "Creating artifact bucket: $ARTIFACT_BUCKET" aws s3 mb "s3://$ARTIFACT_BUCKET" --region "$REGION" fi # Upload JAR to S3 aws s3 cp "target/lambda-java21-1.0.0.jar" \ "s3://$ARTIFACT_BUCKET/lambda-java21/$ENVIRONMENT/" log_info "Artifacts uploaded to S3" } deploy_stack() { log_info "Deploying CloudFormation stack: $STACK_NAME" sam deploy \ --stack-name "$STACK_NAME" \ --parameter-overrides "Environment=$ENVIRONMENT" \ --s3-bucket "$ARTIFACT_BUCKET" \ --s3-prefix "lambda-java21/$ENVIRONMENT" \ --region "$REGION" \ --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ --no-fail-on-empty-changeset \ --tags "Environment=$ENVIRONMENT" "Project=lambda-java21" log_info "Stack deployment completed" } run_tests() { log_info "Running tests..." mvn test log_info "Tests completed successfully" } show_outputs() { log_info "Stack outputs:" aws cloudformation describe-stacks \ --stack-name "$STACK_NAME" \ --query "Stacks[0].Outputs" \ --output table \ --region "$REGION" } # Main execution main() { log_info "Starting deployment for environment: $ENVIRONMENT" log_info "Region: $REGION" log_info "Artifact bucket: $ARTIFACT_BUCKET" build_project run_tests upload_artifacts deploy_stack show_outputs log_info "Deployment completed successfully!" } # Handle errors trap 'log_error "Deployment failed"; exit 1' ERR main

Step 8: Testing with LocalStack

src/test/java/com/example/lambda/UserManagementHandlerTest.java:

package com.example.lambda; import com.example.lambda.handler.UserManagementHandler; import com.amazonaws.services.lambda.runtime.Context; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.localstack.LocalStackContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.*; import java.net.URI; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; @Testcontainers class UserManagementHandlerTest { @Container static LocalStackContainer localStack = new LocalStackContainer( DockerImageName.parse("localstack/localstack:2.0.0")) .withServices(LocalStackContainer.Service.DYNAMODB); private UserManagementHandler handler; private DynamoDbEnhancedClient enhancedClient; @BeforeEach void setUp() { // Create DynamoDB client pointing to LocalStack DynamoDbClient dynamoDbClient = DynamoDbClient.builder() .endpointOverride(localStack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB)) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create(localStack.getAccessKey(), localStack.getSecretKey()))) .region(Region.of(localStack.getRegion())) .build(); this.enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(dynamoDbClient) .build(); this.handler = new UserManagementHandler(enhancedClient); // Create table createUsersTable(dynamoDbClient); } @Test void testCreateUser() { // Given Map<String, Object> input = Map.of( "httpMethod", "POST", "path", "/users", "body", Map.of( "email", "[email protected]", "name", "Test User" ) ); Context context = mock(Context.class); // When Map<String, Object> result = handler.handleRequest(input, context); // Then assertTrue((Boolean) result.get("success")); assertNotNull(((Map<?, ?>) result.get("data")).get("userId")); assertEquals("[email protected]", ((Map<?, ?>) result.get("data")).get("email")); } @Test void testGetUser() { // First create a user Map<String, Object> createInput = Map.of( "httpMethod", "POST", "path", "/users", "body", Map.of( "email", "[email protected]", "name", "Get User" ) ); Context context = mock(Context.class); Map<String, Object> createResult = handler.handleRequest(createInput, context); String userId = (String) ((Map<?, ?>) createResult.get("data")).get("userId"); // Then get the user Map<String, Object> getInput = Map.of( "httpMethod", "GET", "path", "/users/" + userId ); Map<String, Object> getResult = handler.handleRequest(getInput, context); // Verify assertTrue((Boolean) getResult.get("success")); assertEquals(userId, ((Map<?, ?>) getResult.get("data")).get("userId")); assertEquals("[email protected]", ((Map<?, ?>) getResult.get("data")).get("email")); } private void createUsersTable(DynamoDbClient dynamoDbClient) { try { CreateTableRequest request = CreateTableRequest.builder() .tableName("users-test") .keySchema(KeySchemaElement.builder() .attributeName("userId") .keyType(KeyType.HASH) .build()) .attributeDefinitions(AttributeDefinition.builder() .attributeName("userId") .attributeType(ScalarAttributeType.S) .build()) .billingMode(BillingMode.PAY_PER_REQUEST) .build(); dynamoDbClient.createTable(request); // Wait for table to be active dynamoDbClient.waiter().waitUntilTableExists( DescribeTableRequest.builder() .tableName("users-test") .build() ); } catch (ResourceInUseException e) { // Table already exists } } }

Step 9: Performance Optimization Configuration

src/main/resources/log4j2.xml:

<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Lambda name="Lambda"> <PatternLayout> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n</pattern> </PatternLayout> </Lambda> </Appenders> <Loggers> <Root level="INFO"> <AppenderRef ref="Lambda"/> </Root> <Logger name="com.example" level="DEBUG" additivity="false"> <AppenderRef ref="Lambda"/> </Logger> <Logger name="software.amazon.awssdk" level="WARN" additivity="false"> <AppenderRef ref="Lambda"/> </Logger> </Loggers> </Configuration>

Step 10: Usage Examples

Deploy the Application:

# Build and deploy ./build-and-deploy.sh dev # Deploy to production ./build-and-deploy.sh prod # Deploy with custom region AWS_REGION=eu-west-1 ./build-and-deploy.sh staging

Test the Functions:

# Test image processing curl -X POST https://your-api-gateway-url/dev/images/process \ -H "Content-Type: application/json" \ -d '{ "sourceBucket": "source-bucket", "sourceKey": "image.jpg", "destinationBucket": "processed-bucket", "transformations": [ {"type": "thumbnail", "parameters": {"width": "200", "height": "200"}}, {"type": "grayscale", "parameters": {}} ] }' # Test user management curl -X POST https://your-api-gateway-url/dev/users \ -H "Content-Type: application/json" \ -d '{"email": "[email protected]", "name": "John Doe"}' curl https://your-api-gateway-url/dev/users

Monitor Performance:

# View CloudWatch logs aws logs tail "/aws/lambda/user-manager-dev" --follow # Check Lambda metrics aws cloudwatch get-metric-statistics \ --namespace AWS/Lambda \ --metric-name Duration \ --dimensions Name=FunctionName,Value=user-manager-dev \ --start-time 2024-01-01T00:00:00Z \ --end-time 2024-01-02T00:00:00Z \ --period 3600 \ --statistics Average Maximum

Best Practices for Java 21 Lambda

1. Performance Optimization

  • Use Virtual Threads for I/O-bound operations
  • Enable Lambda SnapStart for faster cold starts
  • Optimize JVM flags for serverless environment
  • Use provisioned concurrency for critical functions

2. Memory Management

  • Set appropriate memory size (512MB-3008MB)
  • Use -XX:MaxRAMPercentage=75.0 to leave memory for system
  • Enable -Xshare:on for class data sharing
  • Use -Xss256k to reduce thread stack size

3. Monitoring and Observability

  • Structured logging with JSON format
  • Custom metrics for business KPIs
  • Distributed tracing with X-Ray
  • Function insights for performance analysis

Benefits of Java 21 for Lambda

  1. Faster Cold Starts: Improved startup performance with optimized JVM
  2. Better Concurrency: Virtual threads for massive I/O parallelism
  3. Reduced Memory: Lower memory footprint with better GC
  4. Modern Language Features: Records, pattern matching, sealed classes
  5. Enhanced Developer Experience: Better tooling and debugging support

Conclusion

AWS Lambda with Java 21 runtime provides a powerful platform for building modern serverless applications that are:

  • High-performance with optimized cold starts and execution
  • Cost-effective with efficient resource utilization
  • Scalable with automatic scaling and virtual threads
  • Maintainable with modern Java language features
  • Observable with comprehensive monitoring and tracing

By leveraging Java 21 features like virtual threads, structured concurrency, and records, you can build serverless applications that are both performant and maintainable while taking full advantage of the AWS serverless ecosystem.

Leave a Reply

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


Macro Nepal Helper