
Aws Lambda Java Integration
- 1.4k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
aws-lambda-java-integration is an agent skill for provides aws lambda integration patterns for java with cold start optimization. use when deploying java functions to aws lambda, choosing between micronaut and raw java.
About
The aws-lambda-java-integration skill is designed for provides AWS Lambda integration patterns for Java with cold start optimization. Use when deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java. AWS Lambda Java Integration Patterns for creating high-performance AWS Lambda functions in Java with optimized cold starts. Overview This skill provides complete patterns for AWS Lambda Java development, covering two main approaches: 1. Invoke when the user deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java approaches, optimizing cold starts below 1 second, configuring API Gateway or ALB integration, or implementing serverless Java applications.
- Deploying Java functions to AWS Lambda.
- Optimizing cold starts below 1 second.
- Choosing between Micronaut and Raw Java approaches.
- Configuring API Gateway or ALB integration.
- Setting up CI/CD pipelines for Java Lambda.
Aws Lambda Java Integration by the numbers
- 1,446 all-time installs (skills.sh)
- +55 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #326 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
aws-lambda-java-integration capabilities & compatibility
- Capabilities
- deploying java functions to aws lambda · optimizing cold starts below 1 second · choosing between micronaut and raw java approach · configuring api gateway or alb integration
What aws-lambda-java-integration says it does
Provides AWS Lambda integration patterns for Java with cold start optimization. Use when deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java approaches,
Provides AWS Lambda integration patterns for Java with cold start optimization. Use when deploying Java functions to AWS Lambda, choosing between Micronaut and
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill aws-lambda-java-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I provides aws lambda integration patterns for java with cold start optimization. use when deploying java functions to aws lambda, choosing between micronaut and raw java?
Provides AWS Lambda integration patterns for Java with cold start optimization. Use when deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java.
Who is it for?
Developers using aws lambda java integration workflows documented in SKILL.md.
Skip if: Skip when the task falls outside aws-lambda-java-integration scope or needs a different stack.
When should I use this skill?
User deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java approaches, optimizing cold starts below 1 second, configuring API Gateway or ALB integration, or implementing serverless Java applicat
What you get
Completed aws-lambda-java-integration workflow with documented commands, files, and expected deliverables.
- Gradle build.gradle
- Lambda handler classes
- Shadow JAR deployment artifact
By the numbers
- Micronaut application plugin version 4.7.6
- Gradle shadow plugin version 8.1.1
- 5 documented sections in Micronaut Lambda reference
Files
AWS Lambda Java Integration
Patterns for creating high-performance AWS Lambda functions in Java with optimized cold starts.
Overview
This skill provides complete patterns for AWS Lambda Java development, covering two main approaches:
1. Micronaut Framework - Full-featured framework with AOT compilation, dependency injection, and cold start < 1s 2. Raw Java - Minimal overhead approach with cold start < 500ms
Both approaches support API Gateway and ALB integration with production-ready configurations.
When to Use
- Deploying Java functions to AWS Lambda
- Optimizing cold starts below 1 second
- Choosing between Micronaut and Raw Java approaches
- Configuring API Gateway or ALB integration
- Setting up CI/CD pipelines for Java Lambda
Instructions
1. Choose Your Approach
| Approach | Cold Start | Best For | Complexity |
|---|---|---|---|
| Micronaut | < 1s | Complex apps, DI needed, enterprise | Medium |
| Raw Java | < 500ms | Simple handlers, minimal overhead | Low |
Validate: Confirm the approach fits your use case before proceeding.
2. Project Structure
my-lambda-function/
├── build.gradle (or pom.xml)
├── src/main/java/com/example/Handler.java
└── serverless.yml (or template.yaml)Validate: Verify project structure matches the template.
3. Implementation Examples
Micronaut Handler
@FunctionBean("my-function")
public class MyFunction implements Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final MyService service;
public MyFunction(MyService service) {
this.service = service;
}
@Override
public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent request) {
// Process request
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody("{\"message\": \"Success\"}");
}
}Raw Java Handler
public class MyHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private static final MyService service = new MyService();
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody("{\"message\": \"Success\"}");
}
}Validate: Run sam local invoke to verify handler works before deployment.
Core Patterns
Connection Management
// Initialize once, reuse across invocations
private static final DynamoDbClient dynamoDb = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
// Avoid: Creating clients in handler (slow on every invocation)Error Handling
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
try {
return successResponse(process(request));
} catch (ValidationException e) {
return errorResponse(400, e.getMessage());
} catch (Exception e) {
context.getLogger().log("Error: " + e.getMessage());
return errorResponse(500, "Internal error");
}
}Best Practices
Configuration
- Memory: Start with 512MB, adjust based on profiling
- Timeout: Micronaut 10-30s, Raw Java 5-10s
- Runtime: Java 17 or 21 for best performance
Packaging
- Use Gradle Shadow Plugin or Maven Shade Plugin
- Exclude unnecessary dependencies
Monitoring
- Enable X-Ray tracing for performance analysis
- Use CloudWatch Insights to track cold vs warm starts
Deployment Options
Serverless Framework
service: my-java-lambda
provider:
name: aws
runtime: java21
memorySize: 512
timeout: 10
package:
artifact: build/libs/function.jar
functions:
api:
handler: com.example.Handler
events:
- http:
path: /{proxy+}
method: ANYValidate: Run serverless deploy with --stage dev first.
AWS SAM
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Runtime: java21
MemorySize: 512
Timeout: 10
Events:
ApiEvent:
Type: Api
Properties:
Path: /{proxy+}
Method: ANYValidate: Run sam validate before deploying.
Constraints and Warnings
Java-Specific Constraints
- Reflection: Minimize use; prefer AOT compilation (Micronaut)
- Classpath scanning: Slows cold start; use explicit configuration
- Large frameworks: Spring Boot adds significant cold start overhead
Common Pitfalls
1. Initialization in handler - Causes repeated work on warm invocations 2. Oversized JARs - Include only required dependencies 3. Insufficient memory - Java needs more memory than Node.js/Python 4. No timeout handling - Always set appropriate timeouts
References
For detailed guidance on specific topics:
- [Micronaut Lambda](references/micronaut-lambda.md) - Complete Micronaut setup, AOT configuration, DI optimization
- [Raw Java Lambda](references/raw-java-lambda.md) - Minimal handler patterns, singleton caching, JAR packaging
- [Serverless Deployment](references/serverless-deployment.md) - Serverless Framework, SAM, CI/CD pipelines, provisioned concurrency
- [Testing Lambda](references/testing-lambda.md) - JUnit 5, SAM Local, integration testing, performance measurement
Examples
Example 1: Create a Micronaut Lambda Function
Input:
Create a Java Lambda function using Micronaut to handle user REST APIProcess: 1. Configure Gradle project with Micronaut plugin 2. Create Handler class extending MicronautRequestHandler 3. Implement methods for GET/POST/PUT/DELETE 4. Configure application.yml with AOT optimizations 5. Set up packaging with Shadow plugin 6. Validate: Test locally with SAM CLI before deploying
Output:
- Complete project structure
- Handler with dependency injection
- serverless.yml deployment configuration
Example 2: Optimize Cold Start for Raw Java
Input:
My Java Lambda has 3 second cold start, how do I optimize it?Process: 1. Analyze initialization code 2. Move AWS client creation to static fields 3. Reduce dependencies in build.gradle 4. Configure optimized JVM options 5. Consider provisioned concurrency 6. Validate: Measure cold start with CloudWatch metrics after changes
Output:
- Refactored code with singleton pattern
- Minimized JAR
- Cold start < 500ms
Example 3: Deploy with GitHub Actions
Input:
Configure CI/CD for Java Lambda with SAMProcess: 1. Create GitHub Actions workflow 2. Configure Gradle build with Shadow 3. Set up SAM build and deploy 4. Add test stage before deployment 5. Configure environment protection for prod
Output:
- Complete .github/workflows/deploy.yml
- Multi-stage pipeline (dev/staging/prod)
- Integrated test automation
Version
Version: 1.0.0
Micronaut Lambda Reference
Complete guide for creating AWS Lambda functions with Micronaut Framework.
Table of Contents
1. Project Setup 2. Handler Implementation 3. Cold Start Optimization 4. Dependency Injection 5. Deployment Configuration
---
Project Setup
Gradle Configuration
plugins {
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.micronaut.application") version "4.7.6"
}
version = "1.0.0"
group = "com.example"
repositories {
mavenCentral()
}
dependencies {
// Micronaut Lambda
implementation("io.micronaut.aws:micronaut-function-aws-api-proxy")
implementation("io.micronaut.aws:micronaut-function-aws-custom-runtime")
// AWS Lambda Events
implementation("com.amazonaws:aws-lambda-java-events:3.11.4")
implementation("com.amazonaws:aws-lambda-java-core:1.2.3")
// AWS SDK (if needed)
implementation("software.amazon.awssdk:dynamodb")
implementation("software.amazon.awssdk:s3")
// Testing
testImplementation("io.micronaut.test:micronaut-test-junit5")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core")
}
application {
mainClass.set("com.example.Application")
}
java {
sourceCompatibility = JavaVersion.toVersion("21")
targetCompatibility = JavaVersion.toVersion("21")
}
micronaut {
runtime("lambda")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.example.*")
}
aot {
optimizeClassLoading(true)
convertYamlToJava(true)
precomputeOperations(true)
}
}
tasks.named("shadowJar") {
archiveClassifier.set("")
mergeServiceFiles()
}Maven Configuration
<project>
<parent>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-parent</artifactId>
<version>4.7.6</version>
</parent>
<groupId>com.example</groupId>
<artifactId>my-micronaut-lambda</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<micronaut.version>4.7.6</micronaut.version>
</properties>
<dependencies>
<dependency>
<groupId>io.micronaut.aws</groupId>
<artifactId>micronaut-function-aws-api-proxy</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.11.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.micronaut.maven</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Project Structure
src/
├── main/
│ ├── java/
│ │ └── com/example/
│ │ ├── Application.java
│ │ ├── Handler.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── DynamoDbUserService.java
│ │ └── repository/
│ │ └── UserRepository.java
│ └── resources/
│ ├── application.yml
│ └── logback.xml
└── test/
└── java/
└── com/example/
└── HandlerTest.java---
Handler Implementation
Basic API Gateway Handler
package com.example;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.micronaut.function.aws.MicronautRequestHandler;
import jakarta.inject.Inject;
public class Handler extends MicronautRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Inject
private UserService userService;
@Override
public APIGatewayProxyResponseEvent execute(APIGatewayProxyRequestEvent request) {
String httpMethod = request.getHttpMethod();
String path = request.getPath();
return switch (httpMethod) {
case "GET" -> handleGet(request);
case "POST" -> handlePost(request);
case "PUT" -> handlePut(request);
case "DELETE" -> handleDelete(request);
default -> methodNotAllowed();
};
}
private APIGatewayProxyResponseEvent handleGet(APIGatewayProxyRequestEvent request) {
String userId = request.getPathParameters().get("id");
User user = userService.findById(userId);
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(JsonUtils.toJson(user))
.withHeaders(Map.of("Content-Type", "application/json"));
}
private APIGatewayProxyResponseEvent handlePost(APIGatewayProxyRequestEvent request) {
User user = JsonUtils.fromJson(request.getBody(), User.class);
User created = userService.create(user);
return new APIGatewayProxyResponseEvent()
.withStatusCode(201)
.withBody(JsonUtils.toJson(created))
.withHeaders(Map.of("Content-Type", "application/json"));
}
private APIGatewayProxyResponseEvent methodNotAllowed() {
return new APIGatewayProxyResponseEvent()
.withStatusCode(405)
.withBody("{\"error\": \"Method not allowed\"}");
}
}Stream Handler (Binary Support)
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import io.micronaut.function.aws.runtime.MicronautLambdaRuntime;
import io.micronaut.core.annotation.Nullable;
import java.io.InputStream;
import java.io.OutputStream;
public class StreamHandler implements RequestStreamHandler {
private static final MicronautLambdaRuntime runtime = new MicronautLambdaRuntime();
@Override
public void handleRequest(InputStream input, OutputStream output, Context context) {
runtime.handleRequest(input, output, context);
}
}Function Bean Approach
package com.example;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.micronaut.function.FunctionBean;
import jakarta.inject.Inject;
import java.util.function.Function;
@FunctionBean("user-api")
public class UserApiFunction implements Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Inject
private UserService userService;
@Override
public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent request) {
// Implementation
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody("{}");
}
}---
Cold Start Optimization
AOT Compilation Configuration
# application.yml
micronaut:
application:
name: my-lambda-function
# Disable features not needed in Lambda
server:
enabled: false
# Optimize bean introspection
introspection:
enabled: true
annotations:
- com.example.*
# Disable unnecessary logging overhead
logger:
levels:
io.micronaut.context: WARN
io.micronaut.core: WARNLazy Initialization Pattern
package com.example;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Lazy;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@Factory
public class AwsClientFactory {
@Bean
@Lazy
public DynamoDbClient dynamoDbClient() {
return DynamoDbClient.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.build();
}
@Bean
@Lazy
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.build();
}
}Singleton Services
package com.example.service;
import io.micronaut.context.annotation.Singleton;
import jakarta.annotation.PostConstruct;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@Singleton
public class DynamoDbUserService implements UserService {
private final DynamoDbClient dynamoDb;
private String tableName;
public DynamoDbUserService(DynamoDbClient dynamoDb) {
this.dynamoDb = dynamoDb;
}
@PostConstruct
public void init() {
// Initialization that runs once on first use
this.tableName = System.getenv("USERS_TABLE");
if (tableName == null) {
throw new IllegalStateException("USERS_TABLE environment variable required");
}
}
// Service methods...
}---
Dependency Injection
Service Interfaces and Implementations
// UserService.java
public interface UserService {
User findById(String id);
User create(User user);
void delete(String id);
}
// DynamoDbUserService.java
@Singleton
public class DynamoDbUserService implements UserService {
private final DynamoDbClient dynamoDb;
private final ObjectMapper objectMapper;
@Inject
public DynamoDbUserService(DynamoDbClient dynamoDb, ObjectMapper objectMapper) {
this.dynamoDb = dynamoDb;
this.objectMapper = objectMapper;
}
// Implementation...
}Configuration Properties
package com.example.config;
import io.micronaut.context.annotation.ConfigurationProperties;
@ConfigurationProperties("app")
public class ApplicationConfig {
private String usersTable;
private int defaultPageSize = 20;
private boolean enableCache = true;
// Getters and setters
public String getUsersTable() { return usersTable; }
public void setUsersTable(String usersTable) { this.usersTable = usersTable; }
public int getDefaultPageSize() { return defaultPageSize; }
public void setDefaultPageSize(int defaultPageSize) { this.defaultPageSize = defaultPageSize; }
public boolean isEnableCache() { return enableCache; }
public void setEnableCache(boolean enableCache) { this.enableCache = enableCache; }
}# application.yml
app:
users-table: ${USERS_TABLE}
default-page-size: 20
enable-cache: trueConditional Beans
@Singleton
@Requires(property = "app.enable-cache", value = "true")
public class CachingUserService implements UserService {
// Implementation with caching
}
@Singleton
@Requires(property = "app.enable-cache", value = "false", defaultValue = "false")
public class SimpleUserService implements UserService {
// Implementation without caching
}---
Deployment Configuration
Serverless Framework
service: micronaut-lambda-api
provider:
name: aws
runtime: java21
memorySize: 512
timeout: 10
region: us-east-1
environment:
MICRONAUT_ENVIRONMENTS: lambda
USERS_TABLE: !Ref UsersTable
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
- dynamodb:Scan
- dynamodb:Query
Resource: !GetAtt UsersTable.Arn
package:
artifact: build/libs/my-micronaut-lambda-1.0.0-all.jar
functions:
api:
handler: com.example.Handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASHAWS SAM
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Micronaut Lambda API
Globals:
Function:
Timeout: 10
MemorySize: 512
Runtime: java21
Environment:
Variables:
MICRONAUT_ENVIRONMENTS: lambda
USERS_TABLE: !Ref UsersTable
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/my-micronaut-lambda-1.0.0-all.jar
Handler: com.example.Handler
Events:
ApiEvent:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
ApiUrl:
Description: API Gateway URL
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"Provisioned Concurrency
# serverless.yml with provisioned concurrency
functions:
api:
handler: com.example.Handler
provisionedConcurrency: 5
events:
- http:
path: /{proxy+}
method: ANY# SAM with provisioned concurrency
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5
AutoPublishAlias: live---
Performance Tuning
JVM Options
# serverless.yml
provider:
name: aws
runtime: java21
environment:
JAVA_TOOL_OPTIONS: >
-XX:+TieredCompilation
-XX:TieredStopAtLevel=1
-XX:+UseSerialGC
-Xmx384mBuild Optimization
// build.gradle - minimize JAR size
shadowJar {
minimize {
exclude(dependency('io.micronaut.*'))
exclude(dependency('com.amazonaws.*'))
}
}---
Testing
See testing-lambda.md for comprehensive testing patterns including Micronaut-specific test setup.
Raw Java Lambda Reference
Complete guide for creating minimal AWS Lambda functions in pure Java without frameworks.
Table of Contents
1. Project Structure 2. Minimal Handler 3. Singleton Pattern 4. JAR Packaging 5. Performance Tuning
---
Project Structure
Gradle Configuration
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
group = 'com.example'
version = '1.0.0'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral()
}
dependencies {
// AWS Lambda Core
implementation 'com.amazonaws:aws-lambda-java-core:1.2.3'
implementation 'com.amazonaws:aws-lambda-java-events:3.11.4'
implementation 'com.amazonaws:aws-lambda-java-log4j2:1.5.1'
// JSON Processing (choose one)
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0'
// Or use org.json for minimal size
// implementation 'org.json:json:20231013'
// AWS SDK (optional - include only what you need)
implementation 'software.amazon.awssdk:dynamodb:2.21.0'
// Testing
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-core:5.7.0'
}
test {
useJUnitPlatform()
}
shadowJar {
archiveClassifier = ''
mergeServiceFiles()
}Maven Configuration
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>raw-java-lambda</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>
</properties>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.11.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>Directory Structure
raw-java-lambda/
├── build.gradle (or pom.xml)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/
│ │ │ ├── Handler.java
│ │ │ ├── service/
│ │ │ │ ├── UserService.java
│ │ │ │ └── UserServiceImpl.java
│ │ │ ├── model/
│ │ │ │ └── User.java
│ │ │ └── util/
│ │ │ └── JsonUtil.java
│ │ └── resources/
│ │ └── log4j2.xml
│ └── test/
│ └── java/
│ └── com/example/
│ └── HandlerTest.java
└── serverless.yml (or template.yaml)---
Minimal Handler
Basic Request Handler
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
public class Handler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
// Static initialization for reuse across invocations
private static final ObjectMapper mapper = new ObjectMapper();
private static final UserService userService = new UserServiceImpl();
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
try {
context.getLogger().log("Processing request: " + request.getHttpMethod() + " " + request.getPath());
String method = request.getHttpMethod();
String path = request.getPath();
return switch (method) {
case "GET" -> handleGet(request, context);
case "POST" -> handlePost(request, context);
case "PUT" -> handlePut(request, context);
case "DELETE" -> handleDelete(request, context);
default -> createResponse(405, "{\"error\": \"Method not allowed\"}");
};
} catch (Exception e) {
context.getLogger().log("Error: " + e.getMessage());
return createResponse(500, "{\"error\": \"Internal server error\"}");
}
}
private APIGatewayProxyResponseEvent handleGet(APIGatewayProxyRequestEvent request, Context context) {
String userId = request.getPathParameters().get("id");
User user = userService.findById(userId);
if (user == null) {
return createResponse(404, "{\"error\": \"User not found\"}");
}
try {
String body = mapper.writeValueAsString(user);
return createResponse(200, body);
} catch (Exception e) {
return createResponse(500, "{\"error\": \"Serialization error\"}");
}
}
private APIGatewayProxyResponseEvent handlePost(APIGatewayProxyRequestEvent request, Context context) {
try {
User user = mapper.readValue(request.getBody(), User.class);
User created = userService.create(user);
String body = mapper.writeValueAsString(created);
return createResponse(201, body);
} catch (Exception e) {
return createResponse(400, "{\"error\": \"Invalid request body\"}");
}
}
private APIGatewayProxyResponseEvent handlePut(APIGatewayProxyRequestEvent request, Context context) {
// Implementation
return createResponse(200, "{\"message\": \"Updated\"}");
}
private APIGatewayProxyResponseEvent handleDelete(APIGatewayProxyRequestEvent request, Context context) {
String userId = request.getPathParameters().get("id");
userService.delete(userId);
return createResponse(204, "");
}
private APIGatewayProxyResponseEvent createResponse(int statusCode, String body) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(statusCode)
.withBody(body)
.withHeaders(Map.of(
"Content-Type", "application/json",
"Access-Control-Allow-Origin", "*"
));
}
}Stream Handler (Binary Data)
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class StreamHandler implements RequestStreamHandler {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
try {
// Parse input
JsonNode root = mapper.readTree(inputStream);
String action = root.path("action").asText();
// Process based on action
String result = switch (action) {
case "process" -> processData(root);
case "transform" -> transformData(root);
default -> "{\"error\": \"Unknown action\"}";
};
// Write output
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
String error = "{\"error\": \"" + e.getMessage() + "\"}";
outputStream.write(error.getBytes(StandardCharsets.UTF_8));
}
}
private String processData(JsonNode input) {
// Processing logic
return "{\"status\": \"processed\"}";
}
private String transformData(JsonNode input) {
// Transformation logic
return "{\"status\": \"transformed\"}";
}
}S3 Event Handler
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification;
public class S3Handler implements RequestHandler<S3Event, String> {
@Override
public String handleRequest(S3Event event, Context context) {
for (S3EventNotification.S3EventNotificationRecord record : event.getRecords()) {
String bucket = record.getS3().getBucket().getName();
String key = record.getS3().getObject().getKey();
context.getLogger().log("Processing s3://" + bucket + "/" + key);
// Process the S3 object
processS3Object(bucket, key, context);
}
return "Processed " + event.getRecords().size() + " records";
}
private void processS3Object(String bucket, String key, Context context) {
// Implementation
}
}---
Singleton Pattern
Application Context Cache
package com.example;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.s3.S3Client;
/**
* Application context singleton for caching initialized services.
* Initialized once on first Lambda container creation, reused for warm invocations.
*/
public final class ApplicationContext {
// Singleton instance
private static volatile ApplicationContext instance;
private static final Object lock = new Object();
// Cached clients
private final DynamoDbClient dynamoDbClient;
private final S3Client s3Client;
private final String usersTableName;
private ApplicationContext() {
// Initialize AWS clients
this.dynamoDbClient = DynamoDbClient.create();
this.s3Client = S3Client.create();
// Load configuration from environment
this.usersTableName = System.getenv("USERS_TABLE");
if (usersTableName == null) {
throw new IllegalStateException("USERS_TABLE environment variable required");
}
// Pre-warm connections if needed
validateConnections();
}
public static ApplicationContext getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new ApplicationContext();
}
}
}
return instance;
}
private void validateConnections() {
// Optional: Verify connections work
dynamoDbClient.listTables();
}
public DynamoDbClient getDynamoDbClient() {
return dynamoDbClient;
}
public S3Client getS3Client() {
return s3Client;
}
public String getUsersTableName() {
return usersTableName;
}
}Lazy Initialization Service
package com.example.service;
import com.example.ApplicationContext;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
public class DynamoDbUserService implements UserService {
// Lazy-loaded via singleton
private final DynamoDbClient dynamoDb;
private final String tableName;
public DynamoDbUserService() {
ApplicationContext ctx = ApplicationContext.getInstance();
this.dynamoDb = ctx.getDynamoDbClient();
this.tableName = ctx.getUsersTableName();
}
@Override
public User findById(String id) {
GetItemRequest request = GetItemRequest.builder()
.tableName(tableName)
.key(Map.of("id", AttributeValue.builder().s(id).build()))
.build();
GetItemResponse response = dynamoDb.getItem(request);
if (!response.hasItem()) {
return null;
}
return mapToUser(response.item());
}
@Override
public User create(User user) {
PutItemRequest request = PutItemRequest.builder()
.tableName(tableName)
.item(mapFromUser(user))
.conditionExpression("attribute_not_exists(id)")
.build();
dynamoDb.putItem(request);
return user;
}
@Override
public void delete(String id) {
DeleteItemRequest request = DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of("id", AttributeValue.builder().s(id).build()))
.build();
dynamoDb.deleteItem(request);
}
private User mapToUser(Map<String, AttributeValue> item) {
User user = new User();
user.setId(item.get("id").s());
user.setName(item.get("name").s());
user.setEmail(item.get("email").s());
return user;
}
private Map<String, AttributeValue> mapFromUser(User user) {
Map<String, AttributeValue> item = new HashMap<>();
item.put("id", AttributeValue.builder().s(user.getId()).build());
item.put("name", AttributeValue.builder().s(user.getName()).build());
item.put("email", AttributeValue.builder().s(user.getEmail()).build());
return item;
}
}Handler with Context
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.example.service.DynamoDbUserService;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Handler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
// Static initialization - runs once per container
private static final ObjectMapper mapper = new ObjectMapper();
private static final UserService userService = new DynamoDbUserService();
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
// Log initialization timing
long startTime = System.currentTimeMillis();
try {
// Process request using cached services
String userId = request.getPathParameters().get("id");
User user = userService.findById(userId);
long duration = System.currentTimeMillis() - startTime;
context.getLogger().log("Request processed in " + duration + "ms");
return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(mapper.writeValueAsString(user));
} catch (Exception e) {
context.getLogger().log("Error: " + e.getMessage());
return new APIGatewayProxyResponseEvent()
.withStatusCode(500)
.withBody("{\"error\": \"Internal error\"}");
}
}
}---
JAR Packaging
Minimal JAR Configuration
// build.gradle
shadowJar {
archiveClassifier = ''
mergeServiceFiles()
// Exclude unnecessary files to reduce size
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
exclude 'META-INF/LICENSE*'
exclude 'META-INF/NOTICE*'
exclude 'META-INF/DEPENDENCIES'
// Minimize to remove unused dependencies
minimize {
// Keep these packages even if not directly referenced
exclude(dependency('com.amazonaws:.*'))
exclude(dependency('software.amazon.awssdk:.*'))
exclude(dependency('org.apache.logging.log4j:.*'))
}
}Custom Packaging Script
// build.gradle - custom tasks
tasks.register('packageForLambda', Zip) {
from(shadowJar.outputs)
archiveFileName = "function.zip"
destinationDirectory = layout.buildDirectory.dir('distributions')
}
tasks.register('deployLocal', Copy) {
dependsOn shadowJar
from shadowJar.outputs
into layout.buildDirectory.dir('sam-local')
}Maven Shade Configuration
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>META-INF/NOTICE*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>---
Performance Tuning
JVM Options for Lambda
# serverless.yml
provider:
name: aws
runtime: java21
environment:
# Optimize for short-lived Lambda
JAVA_TOOL_OPTIONS: >-
-XX:+TieredCompilation
-XX:TieredStopAtLevel=1
-XX:+UseSerialGC
-Xmx256m
-XX:MaxMetaspaceSize=128mCustom Runtime (Advanced)
For maximum cold start reduction, consider GraalVM native image:
# Dockerfile for custom runtime
FROM ghcr.io/graalvm/graalvm-ce:ol9-java21-21.0.2 AS builder
WORKDIR /app
COPY . .
RUN ./gradlew nativeCompile
FROM public.ecr.aws/lambda/provided:al2023
COPY --from=builder /app/build/native/nativeCompile/my-app ${LAMBDA_RUNTIME_DIR}/bootstrap
CMD ["com.example.Handler"]Memory Configuration Guide
| JAR Size | Min Memory | Recommended | Notes |
|---|---|---|---|
| < 10MB | 256MB | 512MB | Simple handlers |
| 10-50MB | 512MB | 1024MB | With AWS SDK |
| > 50MB | 1024MB | 1769MB | Large dependencies |
Cold Start Benchmarks
Typical cold start times for Raw Java (512MB):
- Minimal handler (no AWS SDK): ~150-250ms
- With DynamoDB client: ~200-350ms
- With multiple clients: ~300-500ms
Warm start times: typically < 10ms
---
Logging Configuration
<!-- src/main/resources/log4j2.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Lambda name="Lambda">
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1} - %m%n</pattern>
</PatternLayout>
</Lambda>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Lambda"/>
</Root>
<Logger name="software.amazon.awssdk" level="warn"/>
</Loggers>
</Configuration>---
Best Practices Summary
1. Always use static fields for service initialization 2. Keep JAR size minimal - exclude unused dependencies 3. Use Java 21 for best performance 4. Set appropriate memory - start with 512MB 5. Handle exceptions gracefully with proper HTTP status codes 6. Log request IDs for troubleshooting 7. Validate environment variables at startup 8. Reuse AWS clients - don't create per-request
Serverless Deployment Reference
Complete guide for deploying Java Lambda functions with Serverless Framework, AWS SAM, and CI/CD pipelines.
Table of Contents
1. Serverless Framework 2. AWS SAM 3. CI/CD Pipeline 4. Provisioned Concurrency 5. Monitoring 6. Build Optimization 7. Package Optimization 8. Performance Tuning 9. Rollback Strategy 10. Security Best Practices 11. Cost Optimization 12. SAM vs Serverless Framework
---
Serverless Framework
Basic Configuration
service: java-lambda-api
provider:
name: aws
runtime: java21
memorySize: 512
timeout: 10
region: ${opt:region, 'us-east-1'}
stage: ${opt:stage, 'dev'}
environment:
STAGE: ${self:provider.stage}
USERS_TABLE: !Ref UsersTable
iam:
role:
statements:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
- dynamodb:Scan
- dynamodb:Query
Resource: !GetAtt UsersTable.Arn
package:
artifact: build/libs/${self:service}-${self:provider.stage}.jar
functions:
api:
handler: com.example.Handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
- http:
path: /
method: ANY
cors: true
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-users-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
ApiUrl:
Value: !Join ['', ['https://', !Ref ApiGatewayRestApi, '.execute-api.', !Ref 'AWS::Region', '.amazonaws.com/', ${self:provider.stage}]]
Export:
Name: ${self:service}-api-url-${self:provider.stage}Multi-Stage Configuration
service: java-lambda-api
provider:
name: aws
runtime: java21
memorySize: ${self:custom.memorySize.${self:provider.stage}}
timeout: ${self:custom.timeout.${self:provider.stage}}
region: ${opt:region, 'us-east-1'}
stage: ${opt:stage, 'dev'}
environment:
STAGE: ${self:provider.stage}
LOG_LEVEL: ${self:custom.logLevel.${self:provider.stage}}
custom:
memorySize:
dev: 512
staging: 1024
prod: 2048
timeout:
dev: 10
staging: 15
prod: 30
logLevel:
dev: DEBUG
staging: INFO
prod: WARNVPC Configuration
provider:
name: aws
runtime: java21
vpc:
securityGroupIds:
- !Ref LambdaSecurityGroup
subnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
resources:
Resources:
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Lambda Security Group
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']Custom Domain
plugins:
- serverless-domain-manager
custom:
customDomain:
domainName: api.example.com
stage: ${self:provider.stage}
createRoute53Record: true
certificateName: '*.example.com'
endpointType: 'regional'
securityPolicy: tls_1_2---
AWS SAM
Basic Template
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Java Lambda API with SAM
Globals:
Function:
Timeout: 10
MemorySize: 512
Runtime: java21
Architectures:
- x86_64
Environment:
Variables:
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Tags:
Project: MyJavaApi
Parameters:
Stage:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-api'
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Description: Java Lambda API Handler
AutoPublishAlias: live
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ErrorsAlarm
Events:
ApiEvent:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
RestApiId: !Ref ApiGateway
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
- Statement:
- Effect: Allow
Action:
- cloudwatch:PutMetricData
Resource: '*'
ApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: !Sub '${AWS::StackName}-api'
StageName: !Ref Stage
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
AllowOrigin: "'*'"
TracingEnabled: true
MethodSettings:
- ResourcePath: /*
HttpMethod: '*'
LoggingLevel: INFO
DataTraceEnabled: true
MetricsEnabled: true
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub '${AWS::StackName}-users'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
ErrorsAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-errors'
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref ApiFunction
Conditions:
IsProd: !Equals [!Ref Stage, prod]
Outputs:
ApiUrl:
Description: API Gateway URL
Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/'
FunctionArn:
Description: Lambda Function ARN
Value: !GetAtt ApiFunction.Arn
Export:
Name: !Sub '${AWS::StackName}-function-arn'SAM with Layers
Resources:
# Lambda Layer for common dependencies
DependenciesLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: java-dependencies
Description: Common Java dependencies
ContentUri: build/layers/dependencies/
CompatibleRuntimes:
- java21
RetentionPolicy: Retain
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Layers:
- !Ref DependenciesLayer
Environment:
Variables:
JAVA_TOOL_OPTIONS: -cp /opt/java/lib/*:lib/*SAM Local Testing
# samconfig.toml
version = 0.1
[default]
[default.global.parameters]
stack_name = java-lambda-api
[default.build.parameters]
cached = true
parallel = true
[default.validate.parameters]
lint = true
[default.deploy.parameters]
capabilities = CAPABILITY_IAM
confirm_changeset = true
resolve_s3 = true
s3_prefix = java-lambda-api
[default.local_start_api.parameters]
warm_containers = EAGER
[default.local_invoke.parameters]
parameter_overrides = "Stage=local"---
CI/CD Pipeline
GitHub Actions - Full Pipeline
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
JAVA_VERSION: '21'
AWS_REGION: us-east-1
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: gradle
- name: Run tests
run: ./gradlew test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: build/reports/jacoco/test/jacocoTestReport.xml
build:
needs: test
runs-on: ubuntu-latest
outputs:
artifact-path: ${{ steps.build.outputs.artifact-path }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: gradle
- name: Build JAR
id: build
run: |
./gradlew shadowJar
echo "artifact-path=build/libs/$(ls build/libs/*.jar | head -n 1)" >> $GITHUB_OUTPUT
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: lambda-jar
path: build/libs/*.jar
retention-days: 1
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: development
if: github.ref == 'refs/heads/develop'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: lambda-jar
path: build/libs/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy with Serverless
run: |
npm install -g serverless
serverless deploy --stage dev --region $AWS_REGION
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: lambda-jar
path: build/libs/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy with SAM
run: |
sam build
sam deploy \
--stack-name java-api-staging \
--s3-bucket $DEPLOYMENT_BUCKET \
--region $AWS_REGION \
--capabilities CAPABILITY_IAM \
--parameter-overrides Stage=staging \
--no-confirm-changeset
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: lambda-jar
path: build/libs/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to Production
run: |
sam deploy \
--stack-name java-api-prod \
--s3-bucket $DEPLOYMENT_BUCKET \
--region $AWS_REGION \
--capabilities CAPABILITY_IAM \
--parameter-overrides Stage=prod \
--no-confirm-changesetGitHub Actions - SAM Only
# .github/workflows/sam-pipeline.yml
name: SAM Pipeline
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/setup-sam@v2
with:
use-installer: true
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: gradle
- name: Build application
run: ./gradlew shadowJar
- name: SAM Build
run: sam build
- name: SAM Deploy
run: |
sam deploy \
--stack-name java-api-${{ github.event.inputs.environment || 'staging' }} \
--resolve-s3 \
--capabilities CAPABILITY_IAM \
--parameter-overrides Stage=${{ github.event.inputs.environment || 'staging' }} \
--no-confirm-changeset \
--no-fail-on-empty-changeset---
Provisioned Concurrency
Serverless Framework
functions:
api:
handler: com.example.Handler
provisionedConcurrency: 10
events:
- http:
path: /{proxy+}
method: ANY
# Scheduled scaling (optional)
provisionedConcurrencyScalers:
- schedule: cron(0 9 * * ? *)
value: 20
- schedule: cron(0 18 * * ? *)
value: 5SAM Template
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 10
# Scheduled scaling with Application Auto Scaling
ScalableTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 50
MinCapacity: 5
ResourceId: !Sub 'function:${ApiFunction}:live'
RoleARN: !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/lambda.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_LambdaConcurrency'
ScalableDimension: lambda:function:ProvisionedConcurrency
ServiceNamespace: lambda
ScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: lambda-provisioned-concurrency-policy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref ScalableTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 0.7
PredefinedMetricSpecification:
PredefinedMetricType: LambdaProvisionedConcurrencyUtilizationAuto Scaling Based on Schedule
Resources:
BusinessHoursScaleUp:
Type: AWS::AutoScaling::ScheduledAction
Properties:
AutoScalingGroupName: !Ref ScalableTarget
MinSize: 20
MaxSize: 50
Recurrence: 0 9 * * MON-FRI
TimeZone: America/New_York
BusinessHoursScaleDown:
Type: AWS::AutoScaling::ScheduledAction
Properties:
AutoScalingGroupName: !Ref ScalableTarget
MinSize: 5
MaxSize: 50
Recurrence: 0 18 * * MON-FRI
TimeZone: America/New_York---
Monitoring
CloudWatch Alarms
Resources:
HighErrorRateAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-high-error-rate'
AlarmDescription: Error rate exceeds 1%
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 300
EvaluationPeriods: 2
Threshold: 10
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreaching
Dimensions:
- Name: FunctionName
Value: !Ref ApiFunction
HighDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-high-duration'
AlarmDescription: Average duration exceeds 5 seconds
MetricName: Duration
Namespace: AWS/Lambda
Statistic: Average
Period: 300
EvaluationPeriods: 2
Threshold: 5000
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref ApiFunction
ThrottlingAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-throttling'
MetricName: Throttles
Namespace: AWS/Lambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref ApiFunctionCustom Metrics
// Emit custom metrics from Lambda
private void emitMetric(String name, double value, String unit) {
PutMetricDataRequest request = PutMetricDataRequest.builder()
.namespace("MyApplication")
.metricData(MetricDatum.builder()
.metricName(name)
.value(value)
.unit(unit)
.dimensions(
Dimension.builder().name("Function").value("ApiHandler").build(),
Dimension.builder().name("Stage").value(System.getenv("STAGE")).build()
)
.build())
.build();
cloudWatchClient.putMetricData(request);
}
// Usage
emitMetric("ProcessingTime", duration, StandardUnit.MILLISECONDS);
emitMetric("ItemsProcessed", count, StandardUnit.Count);X-Ray Tracing
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Tracing: Active
Environment:
Variables:
AWS_XRAY_CONTEXT_MISSING: LOG_ERROR// Add subsegments for detailed tracing
import com.amazonaws.xray.AWSXRay;
import com.amazonaws.xray.entities.Subsegment;
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
Subsegment subsegment = AWSXRay.beginSubsegment("ProcessRequest");
try {
// Business logic
subsegment.putAnnotation("userId", userId);
subsegment.putMetadata("request", Map.of("path", request.getPath()));
return processRequest(request);
} catch (Exception e) {
subsegment.addException(e);
throw e;
} finally {
AWSXRay.endSubsegment();
}
}CloudWatch Logs Insights Queries
-- Find cold starts
fields @timestamp, @message, @duration
| filter @message like /INIT_START/
| stats count() as cold_starts by bin(5m)
-- Average duration by stage
fields @duration, @memorySize
| filter @type = "REPORT"
| stats avg(@duration), max(@duration), min(@duration) by @memorySize
-- Error analysis
fields @timestamp, @message
| filter @message like /ERROR/
| stats count() as errors by bin(1h)
| sort by @timestamp desc
-- Performance by endpoint (requires custom logging)
fields @timestamp, @message
| parse @message "path: *, duration: *ms" as path, duration
| stats avg(duration), max(duration), count() by path
| sort by avg(duration) desc---
Build Optimization
Gradle Configuration
// build.gradle
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
// Optimized build task for Lambda deployment packages
task buildZip(type: Zip) {
from compileJava
from processResources
into('lib') {
from configurations.runtimeClasspath
}
archiveFileName = "${project.name}-${project.version}.zip"
}
// Shadow JAR with minimized dependencies
shadowJar {
archiveClassifier = ''
mergeServiceFiles()
// Exclude unnecessary files to reduce JAR size
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
exclude 'META-INF/LICENSE*'
exclude 'META-INF/NOTICE*'
// Minimize to only include used classes
minimize()
}
dependencies {
// AWS SDK v2 - include only needed modules
implementation 'software.amazon.awssdk:dynamodb:2.25.0'
implementation 'software.amazon.awssdk:s3:2.25.0'
implementation 'software.amazon.awssdk:lambda:2.25.0'
// Avoid entire AWS SDK - do NOT use:
// implementation 'software.amazon.awssdk:aws-sdk-java:2.25.0'
// Prefer lightweight DI frameworks over Spring
implementation 'com.google.dagger:dagger:2.51'
annotationProcessor 'com.google.dagger:dagger-compiler:2.51'
// Or use Guice if needed
// implementation 'com.google.inject:guice:7.0.0'
// Lambda runtime
implementation 'com.amazonaws:aws-lambda-java-core:1.2.3'
implementation 'com.amazonaws:aws-lambda-java-events:3.11.4'
// Logging (lightweight)
implementation 'org.slf4j:slf4j-simple:2.0.12'
}Maven Configuration
<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
<!-- Maven Shade Plugin for creating uber JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>META-INF/NOTICE*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- AWS SDK v2 - selective modules only -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
<version>2.25.0</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
<!-- Lambda runtime -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>Handler Separation Pattern
// Separate Lambda handler from core business logic
public class LambdaHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final OrderService orderService;
// Constructor for Lambda runtime (with dependency injection)
public LambdaHandler() {
this.orderService = DaggerOrderComponent.create().orderService();
}
// Constructor for testing
public LambdaHandler(OrderService orderService) {
this.orderService = orderService;
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
// Delegate to core logic
return orderService.processOrder(request);
}
}
// Core business logic separate from Lambda infrastructure
public class OrderService {
private final OrderRepository repository;
private final NotificationService notificationService;
@Inject
public OrderService(OrderRepository repository, NotificationService notificationService) {
this.repository = repository;
this.notificationService = notificationService;
}
public APIGatewayProxyResponseEvent processOrder(APIGatewayProxyRequestEvent request) {
// Business logic here - can be tested independently
}
}---
Package Optimization
Lambda Layers for Java Dependencies
# SAM template with Lambda Layers
Resources:
# Shared dependencies layer
DependenciesLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: java-common-dependencies
Description: Common Java dependencies for Lambda functions
ContentUri: build/layers/dependencies/
CompatibleRuntimes:
- java21
RetentionPolicy: Retain
# Function using the layer
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Layers:
- !Ref DependenciesLayer
Environment:
Variables:
JAVA_TOOL_OPTIONS: -cp /opt/java/lib/*:lib/*// build.gradle - Create layer structure
task buildLayer(type: Copy) {
from configurations.runtimeClasspath
into "build/layers/dependencies/java/lib"
// Exclude AWS SDK provided by Lambda runtime
exclude 'aws-lambda-java-core*.jar'
exclude 'aws-lambda-java-events*.jar'
}
// Build minimal function JAR (business logic only)
task buildFunctionJar(type: Jar) {
from sourceSets.main.output
exclude '**/lib/**'
archiveFileName = "function.jar"
}JAR Size Reduction Techniques
// Exclude unnecessary dependencies
dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.16.0') {
// Exclude unused Jackson modules
exclude group: 'com.fasterxml.jackson.module'
}
// Use ProGuard for aggressive optimization (advanced)
// buildscript { dependencies { classpath 'com.guardsquare:proguard-gradle:7.4.0' } }
}
// Gradle configuration for minimal JAR
jar {
enabled = false // Disable standard JAR, use shadow only
}
shadowJar {
// Remove unused classes
minimize {
exclude(dependency('org.slf4j:.*:.*'))
}
// Relocate packages to avoid conflicts
relocate 'com.fasterxml.jackson', 'shaded.com.fasterxml.jackson'
}Maven Dependency Copy for Layer
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/layers/java/lib</outputDirectory>
<includeScope>runtime</includeScope>
<excludeArtifactIds>aws-lambda-java-core,aws-lambda-java-events</excludeArtifactIds>
</configuration>
</execution>
</executions>
</plugin>---
Performance Tuning
JVM Options for Lambda
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: java21
Environment:
Variables:
# Optimized JVM flags for Lambda cold starts
JAVA_TOOL_OPTIONS: >
-XX:+TieredCompilation
-XX:TieredStopAtLevel=1
-XX:+UseSerialGC
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0JVM Option Explanations:
| Option | Purpose | Impact |
|---|---|---|
-XX:+TieredCompilation | Enable tiered compilation | Faster startup |
-XX:TieredStopAtLevel=1 | Stop at C1 compiler only | Reduced compilation time |
-XX:+UseSerialGC | Use single-threaded GC | Lower memory overhead |
-XX:MaxRAMPercentage=75.0 | Limit heap to 75% of container memory | Prevent OOM |
Memory Configuration Guidance
# Memory allocation vs vCPU allocation
# 1769MB = 1 vCPU (linear scaling below this threshold)
Resources:
LowMemoryFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 512
# Suitable for: Simple CRUD, low throughput
BalancedFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 1024
# Good balance of performance and cost
HighPerformanceFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 1769
# 1 full vCPU - optimal for CPU-intensive tasks
MaximumPerformanceFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 3008
# 2 vCPUs - for compute-intensive operationsCold Start Optimization
public class OptimizedHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
// Initialize heavy resources statically (during init phase)
private static final DynamoDbClient dynamoDbClient = DynamoDbClient.builder()
.httpClientBuilder(UrlConnectionHttpClient.builder()) // Lightweight HTTP client
.build();
private static final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
// Initialize during first invocation if needed
private final OrderRepository orderRepository;
public OptimizedHandler() {
// This runs during init phase (not counted in billing)
this.orderRepository = new OrderRepository(dynamoDbClient);
}
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) {
// Handler execution - reuse initialized resources
return processRequest(request);
}
}Connection Pool Settings
// HTTP client configuration for Lambda
public class HttpClientConfig {
public static HttpClient createOptimizedClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.version(HttpClient.Version.HTTP_2)
.build();
}
}
// Database connection pooling (if using RDS Proxy or direct connections)
public class DatabaseConfig {
public static HikariDataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL"));
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
// Lambda-optimized pool settings
config.setMaximumPoolSize(2); // Keep minimal for Lambda
config.setMinimumIdle(0); // Don't maintain idle connections
config.setIdleTimeout(10000); // 10 second idle timeout
config.setConnectionTimeout(5000);
config.setMaxLifetime(300000); // 5 minutes max lifetime
return new HikariDataSource(config);
}
}---
Rollback Strategy
AWS SAM with CodeDeploy Integration
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Runtime: java21
AutoPublishAlias: live # Required for traffic shifting
# Deployment preferences with automatic rollback
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ErrorRateAlarm
- !Ref LatencyAlarm
Hooks:
PreTraffic: !Ref PreTrafficHookFunction
PostTraffic: !Ref PostTrafficHookFunction
# Pre-traffic hook for validation
PreTrafficHookFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/hooks.jar
Handler: com.example.hooks.PreTrafficHook
Runtime: java21
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codedeploy:PutLifecycleEventHookExecutionStatus
Resource: '*'
# Post-traffic hook for verification
PostTrafficHookFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/hooks.jar
Handler: com.example.hooks.PostTrafficHook
Runtime: java21
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codedeploy:PutLifecycleEventHookExecutionStatus
Resource: '*'
# Alarms for automatic rollback triggers
ErrorRateAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-error-rate'
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 60
EvaluationPeriods: 2
Threshold: 5
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref ApiFunction
LatencyAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-latency'
MetricName: Duration
Namespace: AWS/Lambda
Statistic: Average
Period: 60
EvaluationPeriods: 2
Threshold: 3000
ComparisonOperator: GreaterThanThresholdDeployment Types
# Canary deployments - shift traffic gradually
DeploymentPreference:
Type: Canary10Percent5Minutes # 10% for 5 minutes, then 100%
# Other options:
# - Canary10Percent30Minutes
# - Canary10Percent5Minutes
# - Canary10Percent10Minutes
# - Canary10Percent15Minutes
# Linear deployments - shift traffic in increments
DeploymentPreference:
Type: Linear10PercentEvery1Minute # 10% per minute until 100%
# Other options:
# - Linear10PercentEvery2Minutes
# - Linear10PercentEvery3Minutes
# - Linear10PercentEvery10Minutes
# All-at-once (no rollback capability)
DeploymentPreference:
Type: AllAtOncePre/Post Traffic Hook Implementation
// Pre-traffic hook for smoke tests
public class PreTrafficHook implements RequestHandler<Map<String, Object>, String> {
private final CodeDeployClient codeDeployClient;
private final HttpClient httpClient;
public PreTrafficHook() {
this.codeDeployClient = CodeDeployClient.create();
this.httpClient = HttpClient.newHttpClient();
}
@Override
public String handleRequest(Map<String, Object> event, Context context) {
String deploymentId = (String) event.get("DeploymentId");
String lifecycleEventHookExecutionId = (String) event.get("LifecycleEventHookExecutionId");
try {
// Run smoke tests against the new version
boolean testsPassed = runSmokeTests();
// Report status to CodeDeploy
PutLifecycleEventHookExecutionStatusRequest statusRequest =
PutLifecycleEventHookExecutionStatusRequest.builder()
.deploymentId(deploymentId)
.lifecycleEventHookExecutionId(lifecycleEventHookExecutionId)
.status(testsPassed ? LifecycleEventStatus.SUCCEEDED : LifecycleEventStatus.FAILED)
.build();
codeDeployClient.putLifecycleEventHookExecutionStatus(statusRequest);
return testsPassed ? "Success" : "Failure";
} catch (Exception e) {
context.getLogger().log("Pre-traffic hook failed: " + e.getMessage());
// Report failure to trigger rollback
codeDeployClient.putLifecycleEventHookExecutionStatus(
PutLifecycleEventHookExecutionStatusRequest.builder()
.deploymentId(deploymentId)
.lifecycleEventHookExecutionId(lifecycleEventHookExecutionId)
.status(LifecycleEventStatus.FAILED)
.build()
);
throw new RuntimeException(e);
}
}
private boolean runSmokeTests() {
// Implement smoke tests (health check, basic functionality)
return true;
}
}---
Security Best Practices
IAM Least Privilege
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Policies:
# Use managed policies for common patterns
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
# Custom least-privilege policy
- Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource:
- !Sub 'arn:aws:s3:::${BucketName}/uploads/*'
Condition:
StringEquals:
s3:x-amz-acl: bucket-owner-full-control
# Explicit deny for sensitive operations
- Effect: Deny
Action:
- s3:DeleteBucket
- dynamodb:DeleteTable
Resource: '*'VPC Configuration
Resources:
VpcFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
# VPC functions require NAT Gateway for internet access
# or VPC endpoints for AWS services
# Security group with minimal access
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Lambda security group
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS to AWS services
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
DestinationSecurityGroupId: !Ref RdsSecurityGroup
Description: PostgreSQL to RDS
# VPC Endpoints for AWS services (avoid NAT Gateway costs)
S3VpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateRouteTable
DynamoDbVpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.dynamodb'
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateRouteTableSecrets Manager and SSM Parameter Store
Resources:
SecureFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Environment:
Variables:
# Reference secrets by ARN (not value)
DB_SECRET_ARN: !Ref DatabaseSecret
API_KEY_PARAM: !Ref ApiKeyParameter
Policies:
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref DatabaseSecret
- Effect: Allow
Action:
- ssm:GetParameter
Resource: !Ref ApiKeyParameter
# Secrets Manager for database credentials
DatabaseSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub '/${AWS::StackName}/database-credentials'
Description: RDS database credentials
GenerateSecretString:
SecretStringTemplate: '{"username": "dbadmin"}'
GenerateStringKey: password
PasswordLength: 32
ExcludeCharacters: '"@/\\'
KmsKeyId: !Ref SecretsKmsKey
# SSM Parameter Store for configuration
ApiKeyParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub '/${AWS::StackName}/api-key'
Type: SecureString
Value: placeholder-replace-manually
Description: External API key
Tier: Standard
# KMS key for encryption
SecretsKmsKey:
Type: AWS::KMS::Key
Properties:
Description: KMS key for Lambda secrets
EnableKeyRotation: true
KeyPolicy:
Version: '2012-10-17'
Statement:
- Sid: Enable IAM User Permissions
Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'kms:*'
Resource: '*'// Retrieve secrets in Lambda with caching
public class SecretCache {
private final SecretsManagerClient secretsClient;
private final Map<String, String> cache = new ConcurrentHashMap<>();
private final long cacheTtlMs = 300000; // 5 minutes
public String getSecret(String secretArn) {
return cache.computeIfAbsent(secretArn, arn -> {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(arn)
.build();
GetSecretValueResponse response = secretsClient.getSecretValue(request);
return response.secretString();
});
}
// Parse JSON secrets
public DatabaseCredentials getDatabaseCredentials(String secretArn) {
String secretString = getSecret(secretArn);
return new ObjectMapper().readValue(secretString, DatabaseCredentials.class);
}
}Resource-Based Policies
Resources:
InternalFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
# Explicit resource policy for cross-account or service access
FunctionResourcePolicy:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref InternalFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*'
# Service-specific invocation permission
S3InvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref S3ProcessorFunction
Action: lambda:InvokeFunction
Principal: s3.amazonaws.com
SourceArn: !Sub 'arn:aws:s3:::${BucketName}'
SourceAccount: !Ref AWS::AccountId---
Cost Optimization
Graviton2 (ARM64) Support
Resources:
ArmFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/libs/function.jar
Handler: com.example.Handler
Runtime: java21
Architectures:
- arm64 # Graviton2 - up to 34% better price-performance
# Build configuration for ARM64
# Gradle: No changes needed - Java bytecode is architecture-independent
# But native dependencies must be ARM64 compatible// build.gradle - Multi-architecture build support
plugins {
id 'java'
}
dependencies {
// AWS SDK CRT client for ARM64 optimization
implementation 'software.amazon.awssdk:aws-crt-client:2.25.0'
// Ensure all native dependencies have ARM64 support
implementation 'io.netty:netty-transport-native-epoll:4.1.107.Final:linux-aarch_64'
}Memory Size Tuning
# Find optimal memory/price ratio using AWS Lambda Power Tuning
Resources:
TunedFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 1769 # Sweet spot: 1 vCPU, good performance/cost ratio
# Test different values: 512, 1024, 1769, 3008
# Use Lambda Power Tuning tool to find optimal setting
# https://github.com/alexcasalboni/aws-lambda-power-tuningProvisioned Concurrency Cost Analysis
# Cost comparison: On-demand vs Provisioned Concurrency
# On-demand (pay per invocation):
# - $0.20 per 1M requests
# - $0.0000166667 per GB-second
# - Cold starts on scale-up
# Provisioned Concurrency (predictable performance):
# - $0.20 per 1M requests (same)
# - $0.000004646 per GB-second (lower compute cost)
# - $0.000004646 per GB-second provisioned (base cost)
Resources:
# Use for predictable, latency-sensitive workloads
HighTrafficFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 1024
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 10
# Scheduled scaling to optimize cost
ScalableTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MinCapacity: 2 # Minimum during off-hours
MaxCapacity: 50 # Maximum during peakLambda Layers for Shared Dependencies
# Share common dependencies across functions to reduce deployment size
Resources:
SharedLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: shared-java-deps
ContentUri: layers/shared/
CompatibleRuntimes:
- java21
FunctionA:
Type: AWS::Serverless::Function
Properties:
CodeUri: function-a/build/distributions/function-a.zip
Handler: com.example.FunctionAHandler
Layers:
- !Ref SharedLayer
# Smaller deployment package = faster cold starts
FunctionB:
Type: AWS::Serverless::Function
Properties:
CodeUri: function-b/build/distributions/function-b.zip
Handler: com.example.FunctionBHandler
Layers:
- !Ref SharedLayer
# Reuses same layer, no duplication---
SAM vs Serverless Framework
| Feature | SAM | Serverless Framework |
|---|---|---|
| Native AWS | Yes - AWS-maintained | No - Third-party |
| Build system | Gradle/Maven native | Plugin-based (Gradle plugin available) |
| Local testing | SAM CLI (local invoke, local API) | serverless-offline |
| Java support | Native first-class | Via plugins |
| Deployment | CloudFormation-based | CloudFormation-based |
| Syntax | YAML/JSON | YAML |
| CI/CD integration | Native GitHub Actions, CodePipeline | Extensive plugin ecosystem |
| Rollback | Built-in CodeDeploy integration | Manual or custom scripts |
| IDE support | AWS Toolkit (IntelliJ, VS Code) | Limited |
| Debugging | Local debugging with breakpoints | Limited |
| Velocity macros | Supported | Not applicable |
When to Choose SAM
- Native AWS tooling and support
- Deep Java integration with build tools
- Built-in safe deployments with rollback
- Local testing with SAM CLI
- AWS IDE toolkit integration
When to Choose Serverless Framework
- Multi-cloud requirements (Azure, GCP)
- Large plugin ecosystem needed
- Team familiar with Node.js tooling
- Complex event source configurations
- Non-AWS resource management
---
Deployment Commands
Serverless
# Deploy to specific stage
serverless deploy --stage prod --region us-east-1
# Deploy single function
serverless deploy function -f api
# Package without deploying
serverless package --stage prod
# View logs
serverless logs -f api --tail
# Remove stack
serverless remove --stage prodSAM
# Build
sam build
# Local test
sam local invoke ApiFunction -e events/test.json
sam local start-api
# Deploy
sam deploy --guided
sam deploy --no-confirm-changeset
# View logs
sam logs -n ApiFunction --tail
# Delete stack
sam delete --stack-name my-stack---
Environment Variables Management
SSM Parameter Store
provider:
environment:
DATABASE_URL: ${ssm:/my-app/${self:provider.stage}/database-url}
API_KEY: ${ssm:/my-app/${self:provider.stage}/api-key~true} # SecureStringSecrets Manager
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
DB_SECRET_ARN: !Ref DatabaseSecret
Policies:
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref DatabaseSecret
DatabaseSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub '/${AWS::StackName}/database-credentials'
GenerateSecretString:
SecretStringTemplate: '{"username": "admin"}'
GenerateStringKey: password
PasswordLength: 16// Retrieve secret in Lambda
private String getSecret(String secretName) {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(secretName)
.build();
GetSecretValueResponse response = secretsManager.getSecretValue(request);
return response.secretString();
}Testing Lambda Reference
Complete guide for testing AWS Lambda Java functions including unit tests, integration tests, and performance testing.
Table of Contents
1. Unit Testing 2. Integration Testing 3. Performance Testing 4. Test Automation
---
Unit Testing
JUnit 5 Setup
// build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-core:5.7.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.7.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}Handler Unit Test
package com.example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class HandlerTest {
@Mock
private UserService userService;
@Mock
private Context context;
@InjectMocks
private Handler handler;
private final ObjectMapper mapper = new ObjectMapper();
@BeforeEach
void setUp() {
when(context.getLogger()).thenReturn(System.out::println);
}
@Test
void shouldReturnUser_whenGetRequest() throws Exception {
// Given
String userId = "123";
User mockUser = new User(userId, "John Doe", "john@example.com");
when(userService.findById(userId)).thenReturn(mockUser);
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent()
.withHttpMethod("GET")
.withPath("/users/" + userId)
.withPathParameters(Map.of("id", userId));
// When
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);
// Then
assertThat(response.getStatusCode()).isEqualTo(200);
assertThat(response.getBody()).contains("John Doe");
verify(userService).findById(userId);
}
@Test
void shouldCreateUser_whenPostRequest() throws Exception {
// Given
User newUser = new User(null, "Jane Doe", "jane@example.com");
User createdUser = new User("456", "Jane Doe", "jane@example.com");
when(userService.create(any(User.class))).thenReturn(createdUser);
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent()
.withHttpMethod("POST")
.withPath("/users")
.withBody(mapper.writeValueAsString(newUser));
// When
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);
// Then
assertThat(response.getStatusCode()).isEqualTo(201);
assertThat(response.getBody()).contains("456");
}
@Test
void shouldReturn404_whenUserNotFound() {
// Given
String userId = "999";
when(userService.findById(userId)).thenReturn(null);
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent()
.withHttpMethod("GET")
.withPath("/users/" + userId)
.withPathParameters(Map.of("id", userId));
// When
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);
// Then
assertThat(response.getStatusCode()).isEqualTo(404);
assertThat(response.getBody()).contains("User not found");
}
@Test
void shouldReturn405_forUnsupportedMethod() {
// Given
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent()
.withHttpMethod("PATCH")
.withPath("/users/123");
// When
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);
// Then
assertThat(response.getStatusCode()).isEqualTo(405);
}
}Service Layer Testing
package com.example.service;
import com.example.model.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DynamoDbUserServiceTest {
@Mock
private DynamoDbClient dynamoDbClient;
private DynamoDbUserService userService;
@BeforeEach
void setUp() {
userService = new DynamoDbUserService(dynamoDbClient, "test-table");
}
@Test
void shouldReturnUser_whenFound() {
// Given
String userId = "123";
Map<String, AttributeValue> item = Map.of(
"id", AttributeValue.builder().s(userId).build(),
"name", AttributeValue.builder().s("John Doe").build(),
"email", AttributeValue.builder().s("john@example.com").build()
);
when(dynamoDbClient.getItem(any(GetItemRequest.class)))
.thenReturn(GetItemResponse.builder().item(item).build());
// When
User user = userService.findById(userId);
// Then
assertThat(user).isNotNull();
assertThat(user.getId()).isEqualTo(userId);
assertThat(user.getName()).isEqualTo("John Doe");
}
@Test
void shouldReturnNull_whenNotFound() {
// Given
when(dynamoDbClient.getItem(any(GetItemRequest.class)))
.thenReturn(GetItemResponse.builder().build());
// When
User user = userService.findById("999");
// Then
assertThat(user).isNull();
}
@Test
void shouldThrowException_whenDynamoDbFails() {
// Given
when(dynamoDbClient.getItem(any(GetItemRequest.class)))
.thenThrow(DynamoDbException.builder().message("Connection failed").build());
// Then
assertThatThrownBy(() -> userService.findById("123"))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Connection failed");
}
}Micronaut Handler Testing
package com.example;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@MicronautTest
class MicronautHandlerTest {
@Inject
private Handler handler;
@Test
void testHandler() {
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent()
.withHttpMethod("GET")
.withPath("/test");
APIGatewayProxyResponseEvent response = handler.execute(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode());
}
}Parameterized Tests
package com.example;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
class ValidationTest {
@ParameterizedTest
@ValueSource(strings = {"user@example.com", "test@test.org", "admin@company.co.uk"})
void shouldAcceptValidEmails(String email) {
assertThat(EmailValidator.isValid(email)).isTrue();
}
@ParameterizedTest
@ValueSource(strings = {"invalid", "@example.com", "user@", ""})
void shouldRejectInvalidEmails(String email) {
assertThat(EmailValidator.isValid(email)).isFalse();
}
@ParameterizedTest
@CsvSource({
"GET, /users, 200",
"POST, /users, 201",
"DELETE, /users/123, 204",
"PATCH, /users/123, 405"
})
void shouldReturnExpectedStatus(String method, String path, int expectedStatus) {
// Test implementation
}
}---
Integration Testing
SAM Local Testing
# Install SAM CLI
brew install aws-sam-cli
# Build the application
sam build
# Invoke function locally
sam local invoke ApiFunction -e events/api-request.json
# Start local API Gateway
sam local start-api --warm-containers EAGERSAM Event Files
// events/get-user.json
{
"httpMethod": "GET",
"path": "/users/123",
"pathParameters": {
"id": "123"
},
"headers": {
"Content-Type": "application/json"
},
"requestContext": {
"requestId": "test-request-id",
"stage": "local"
}
}// events/post-user.json
{
"httpMethod": "POST",
"path": "/users",
"headers": {
"Content-Type": "application/json"
},
"body": "{\"name\": \"John Doe\", \"email\": \"john@example.com\"}"
}TestContainers Integration
package com.example;
import com.example.service.DynamoDbUserService;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
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.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.DYNAMODB;
@Testcontainers
class DynamoDbIntegrationTest {
@Container
static LocalStackContainer localStack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.0"))
.withServices(DYNAMODB);
private static DynamoDbClient dynamoDbClient;
private static DynamoDbUserService userService;
@BeforeAll
static void setUp() {
dynamoDbClient = DynamoDbClient.builder()
.endpointOverride(localStack.getEndpointOverride(DYNAMODB))
.region(Region.of(localStack.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.build();
// Create table
createUsersTable();
userService = new DynamoDbUserService(dynamoDbClient, "users");
}
private static void createUsersTable() {
dynamoDbClient.createTable(builder -> builder
.tableName("users")
.attributeDefinitions(def -> def
.attributeName("id")
.attributeType(ScalarAttributeType.S))
.keySchema(key -> key
.attributeName("id")
.keyType(KeyType.HASH))
.billingMode(BillingMode.PAY_PER_REQUEST));
}
@AfterAll
static void tearDown() {
if (dynamoDbClient != null) {
dynamoDbClient.close();
}
}
@Test
void shouldCreateAndRetrieveUser() {
// Given
User user = new User("123", "John Doe", "john@example.com");
// When
User created = userService.create(user);
User retrieved = userService.findById("123");
// Then
assertThat(created).isNotNull();
assertThat(retrieved).isNotNull();
assertThat(retrieved.getName()).isEqualTo("John Doe");
}
}Docker Compose for Local Testing
# docker-compose.test.yml
version: '3.8'
services:
localstack:
image: localstack/localstack:3.0
ports:
- "4566:4566"
environment:
- SERVICES=dynamodb,s3,lambda
- DEFAULT_REGION=us-east-1
- DEBUG=1
volumes:
- ./init-scripts:/etc/localstack/init/ready.d
test-runner:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
- localstack
environment:
- AWS_ENDPOINT=http://localstack:4566
- AWS_REGION=us-east-1# Dockerfile.test
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY . .
RUN ./gradlew testClasses --no-daemon
CMD ["./gradlew", "integrationTest", "--no-daemon"]---
Performance Testing
Cold Start Measurement
package com.example.performance;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.lambda.LambdaClient;
import software.amazon.awssdk.services.lambda.model.*;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
class ColdStartTest {
private final LambdaClient lambda = LambdaClient.create();
@Test
void measureColdStart() throws InterruptedException {
String functionName = "my-java-lambda";
// Force cold start by updating function configuration
lambda.updateFunctionConfiguration(UpdateFunctionConfigurationRequest.builder()
.functionName(functionName)
.description("Cold start test at " + Instant.now())
.build());
// Wait for update
waitForFunctionUpdate(functionName);
// Invoke and measure
long startTime = System.currentTimeMillis();
InvokeResponse response = lambda.invoke(InvokeRequest.builder()
.functionName(functionName)
.payload("{\"test\": true}")
.build());
long duration = System.currentTimeMillis() - startTime;
// Parse response for init duration
String payload = response.payload().asUtf8String();
System.out.println("Total duration: " + duration + "ms");
System.out.println("Response: " + payload);
// Cold start should be under 500ms for Raw Java
assertThat(duration).isLessThan(1000);
}
private void waitForFunctionUpdate(String functionName) throws InterruptedException {
while (true) {
GetFunctionResponse response = lambda.getFunction(GetFunctionRequest.builder()
.functionName(functionName)
.build());
if (response.configuration().lastUpdateStatus() == LastUpdateStatus.SUCCESSFUL) {
break;
}
TimeUnit.SECONDS.sleep(1);
}
}
}Load Testing with JMH
package com.example.benchmark;
import com.example.Handler;
import com.example.service.UserService;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Fork(2)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class HandlerBenchmark {
private Handler handler;
private APIGatewayProxyRequestEvent request;
@Setup
public void setup() {
handler = new Handler();
request = new APIGatewayProxyRequestEvent()
.withHttpMethod("GET")
.withPath("/users/123")
.withPathParameters(Map.of("id", "123"));
}
@Benchmark
public APIGatewayProxyResponseEvent testHandleRequest() {
return handler.handleRequest(request, new TestContext());
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(HandlerBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}AWS Lambda Power Tuning
# power-tuning.py - using AWS Lambda Power Tuning tool
import boto3
import json
def run_power_tuning():
lambda_client = boto3.client('lambda')
stepfunctions = boto3.client('stepfunctions')
# Power tuning configuration
payload = {
"lambdaARN": "arn:aws:lambda:us-east-1:123456789:function:my-java-lambda",
"powerValues": [128, 256, 512, 1024, 2048],
"num": 50,
"payload": "{}",
"parallelInvocation": True,
"strategy": "speed"
}
# Start power tuning state machine
response = stepfunctions.start_execution(
stateMachineArn="arn:aws:states:us-east-1:123456789:stateMachine:powerTuningStateMachine",
input=json.dumps(payload)
)
print(f"Power tuning started: {response['executionArn']}")CloudWatch Metrics Collection
package com.example.performance;
import software.amazon.awssdk.services.cloudwatch.CloudWatchClient;
import software.amazon.awssdk.services.cloudwatch.model.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class MetricsCollector {
private final CloudWatchClient cloudWatch = CloudWatchClient.create();
public void analyzePerformance(String functionName, int hours) {
Instant endTime = Instant.now();
Instant startTime = endTime.minus(hours, ChronoUnit.HOURS);
// Get duration metrics
GetMetricStatisticsRequest durationRequest = GetMetricStatisticsRequest.builder()
.namespace("AWS/Lambda")
.metricName("Duration")
.dimensions(
Dimension.builder().name("FunctionName").value(functionName).build())
.startTime(startTime)
.endTime(endTime)
.period(300)
.statistics(Statistic.AVERAGE, Statistic.MAXIMUM, Statistic.P99)
.build();
GetMetricStatisticsResponse durationResponse = cloudWatch.getMetricStatistics(durationRequest);
System.out.println("Duration Statistics:");
for (Datapoint dp : durationResponse.datapoints()) {
System.out.printf(" Average: %.2fms, Max: %.2fms, P99: %.2fms%n",
dp.average(), dp.maximum(), dp.extendedStatistics().get("p99"));
}
// Get error metrics
GetMetricStatisticsRequest errorRequest = GetMetricStatisticsRequest.builder()
.namespace("AWS/Lambda")
.metricName("Errors")
.dimensions(
Dimension.builder().name("FunctionName").value(functionName).build())
.startTime(startTime)
.endTime(endTime)
.period(3600)
.statistics(Statistic.SUM)
.build();
GetMetricStatisticsResponse errorResponse = cloudWatch.getMetricStatistics(errorRequest);
long totalErrors = errorResponse.datapoints().stream()
.mapToLong(dp -> dp.sum().longValue())
.sum();
System.out.println("Total Errors: " + totalErrors);
}
}---
Test Automation
Gradle Test Configuration
// build.gradle
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
showExceptions true
showCauses true
showStackTraces true
}
reports {
html.required = true
junitXml.required = true
}
}
// Integration test source set
sourceSets {
integrationTest {
java {
srcDir 'src/integration-test/java'
}
resources {
srcDir 'src/integration-test/resources'
}
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
}
tasks.register('integrationTest', Test) {
description = 'Runs integration tests'
group = 'verification'
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
shouldRunAfter test
}
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
// Code coverage
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.11"
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
html.required = true
}
}Test Reporting
// Generate aggregate test report
tasks.register('testReport', TestReport) {
destinationDirectory = layout.buildDirectory.dir('reports/allTests')
testResults.from(
test,
integrationTest
)
}CI/CD Test Integration
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Run unit tests
run: ./gradlew test
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: build/reports/tests/
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: build/reports/jacoco/test/jacocoTestReport.xml
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Start LocalStack
run: |
docker run -d \
-p 4566:4566 \
-e SERVICES=dynamodb,s3 \
localstack/localstack:3.0
- name: Wait for LocalStack
run: |
sleep 10
curl -s http://localhost:4566/_localstack/health
- name: Run integration tests
run: ./gradlew integrationTest
env:
AWS_ENDPOINT: http://localhost:4566
AWS_REGION: us-east-1
- name: Upload integration test results
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-test-results
path: build/reports/tests/integrationTest/Blue-Green Deployment Testing
package com.example.deployment;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.lambda.LambdaClient;
import software.amazon.awssdk.services.lambda.model.*;
import static org.assertj.core.api.Assertions.assertThat;
class BlueGreenDeploymentTest {
private final LambdaClient lambda = LambdaClient.create();
@Test
void canaryDeploymentShouldSucceed() {
String functionName = "my-java-lambda";
String aliasName = "live";
// Publish new version
PublishVersionResponse versionResponse = lambda.publishVersion(
PublishVersionRequest.builder()
.functionName(functionName)
.description("Canary test version")
.build());
String newVersion = versionResponse.version();
// Update alias to point 10% to new version
lambda.updateAlias(UpdateAliasRequest.builder()
.functionName(functionName)
.name(aliasName)
.functionVersion(newVersion)
.routingConfig(AliasRoutingConfiguration.builder()
.additionalVersionWeights(Map.of("1", 0.9)) // 90% old, 10% new
.build())
.build());
// Run smoke tests
boolean smokeTestPassed = runSmokeTests(functionName, aliasName);
assertThat(smokeTestPassed).isTrue();
// Shift traffic to 100% new version
lambda.updateAlias(UpdateAliasRequest.builder()
.functionName(functionName)
.name(aliasName)
.functionVersion(newVersion)
.build());
}
private boolean runSmokeTests(String functionName, String aliasName) {
// Invoke function multiple times and check responses
for (int i = 0; i < 10; i++) {
InvokeResponse response = lambda.invoke(InvokeRequest.builder()
.functionName(functionName + ":" + aliasName)
.payload("{\"test\": true}")
.build());
if (response.statusCode() != 200) {
return false;
}
}
return true;
}
}---
Best Practices
Test Organization
src/
├── main/java/com/example/
│ ├── Handler.java
│ └── service/
├── test/java/com/example/
│ ├── HandlerTest.java # Unit tests
│ ├── service/
│ │ └── UserServiceTest.java
│ └── util/
│ └── JsonUtilTest.java
└── integration-test/java/com/example/
├── HandlerIntegrationTest.java
└── service/
└── DynamoDbServiceIT.javaTest Data Builders
package com.example.test;
import com.example.model.User;
public class UserBuilder {
private String id = "123";
private String name = "John Doe";
private String email = "john@example.com";
public static UserBuilder aUser() {
return new UserBuilder();
}
public UserBuilder withId(String id) {
this.id = id;
return this;
}
public UserBuilder withName(String name) {
this.name = name;
return this;
}
public UserBuilder withEmail(String email) {
this.email = email;
return this;
}
public User build() {
return new User(id, name, email);
}
}
// Usage in tests
User user = UserBuilder.aUser()
.withId("456")
.withName("Jane Doe")
.build();Test Context Mock
package com.example.test;
import com.amazonaws.services.lambda.runtime.ClientContext;
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
public class TestContext implements Context {
@Override
public String getAwsRequestId() {
return "test-request-id";
}
@Override
public String getLogGroupName() {
return "/aws/lambda/test-function";
}
@Override
public String getLogStreamName() {
return "2024/01/01/test-stream";
}
@Override
public String getFunctionName() {
return "test-function";
}
@Override
public String getFunctionVersion() {
return "$LATEST";
}
@Override
public String getInvokedFunctionArn() {
return "arn:aws:lambda:us-east-1:123456789:function:test-function";
}
@Override
public CognitoIdentity getIdentity() {
return null;
}
@Override
public ClientContext getClientContext() {
return null;
}
@Override
public int getRemainingTimeInMillis() {
return 30000;
}
@Override
public int getMemoryLimitInMB() {
return 512;
}
@Override
public LambdaLogger getLogger() {
return System.out::println;
}
}Related skills
How it compares
Pick aws-lambda-java-integration over generic AWS skills when the runtime is Java with Micronaut DI and shadow JAR Lambda packaging.
FAQ
What does aws-lambda-java-integration do?
Provides AWS Lambda integration patterns for Java with cold start optimization. Use when deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java.
When should I use aws-lambda-java-integration?
User deploying Java functions to AWS Lambda, choosing between Micronaut and Raw Java approaches, optimizing cold starts below 1 second, configuring API Gateway or ALB integration, or implementing serverless Java applications.
Is aws-lambda-java-integration safe to install?
Review the Security Audits panel on this page before installing in production.