Contents
- Deployment Approaches
- Lambda Web Adapter — Full Spring Boot
- Native Function Handler
- AWS SnapStart
- SAM CLI — Local Testing & Deployment
- Cold Start Optimisation
- Configuration & Environment Variables
- Observability — Logs, Tracing & Metrics
There are two practical ways to run Spring Boot code on AWS Lambda. Choosing the right one depends on whether you are migrating an existing HTTP API or building a purpose-built serverless function.
| Approach | How it works | Best for |
| Lambda Web Adapter | A thin Lambda extension that proxies API Gateway events to an HTTP server running on localhost:8080 inside the same execution environment | Migrating existing Spring Boot REST APIs; familiar MVC/WebFlux code with no changes |
| Function Handler | Implement RequestHandler<I,O> or Spring Cloud Function's Function<I,O>; Spring context starts on first invocation | Lightweight event processors — SQS, SNS, EventBridge triggers with simple logic |
Both approaches benefit from SnapStart. Lambda Web Adapter is the lower-friction path for teams already writing Spring Boot services and wanting serverless deployment without rewriting their code.
The AWS Lambda Web Adapter (open source, maintained by AWS) wraps any HTTP server process. It starts your Spring Boot app as a subprocess listening on port 8080, then translates Lambda invocation events (API Gateway v1/v2, ALB) into HTTP requests and sends responses back. From Spring's perspective it is a normal HTTP server startup.
<!-- pom.xml — standard Spring Boot Web dependency; no Lambda SDK needed -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// Standard Spring Boot application — zero Lambda-specific code
@SpringBootApplication
public class ProductApi {
public static void main(String[] args) {
SpringApplication.run(ProductApi.class, args);
}
}
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@PostMapping
public ResponseEntity<Product> create(@RequestBody @Valid Product product) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(productService.save(product));
}
}
# Dockerfile — include Lambda Web Adapter extension layer
FROM public.ecr.aws/lambda/java:21
# Copy the Lambda Web Adapter binary from the official layer
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 \
/lambda-adapter /opt/extensions/lambda-adapter
# Tell the adapter which port Spring Boot listens on (default 8080)
ENV PORT=8080
ENV AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap
COPY target/product-api-1.0.jar ${LAMBDA_TASK_ROOT}/app.jar
CMD ["software.amazon.awssdk.lambda.LambdaBootstrap"]
For event-driven workloads (SQS, SNS, S3 triggers) where the overhead of a full MVC stack is unnecessary, implement a slim Spring-managed RequestHandler using Spring Cloud Function. The framework wires the Spring context once and routes invocations to a Function bean by name.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
@SpringBootApplication
public class OrderProcessor {
public static void main(String[] args) {
SpringApplication.run(OrderProcessor.class, args);
}
// Spring Cloud Function — the Lambda handler routes here by function name
@Bean
public Function<Order, ProcessingResult> processOrder(OrderService svc) {
return order -> {
svc.validate(order);
svc.save(order);
svc.publishEvent(order);
return new ProcessingResult("OK", order.getId());
};
}
}
# application.yml
spring:
cloud:
function:
definition: processOrder # tells the AWS adapter which Function bean to invoke
# Lambda configuration
Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
Runtime: java21
Memory: 512 MB
Timeout: 30 s
SnapStart works by taking a Firecracker MicroVM snapshot of the Lambda execution environment after the Init phase (class loading + Spring context startup) completes. Subsequent invocations restore from the snapshot instead of re-initialising, reducing cold start latency from 5–15 seconds (typical Spring Boot on JVM) to under 1 second.
- Available for Java 11+ managed runtimes and container images with Lambda Web Adapter
- Enabled per Lambda function via the SnapStart configuration setting
- Each new published version gets a fresh snapshot — snapshots are not shared between versions
- Free — no additional cost beyond normal Lambda pricing
# SAM template.yaml — enable SnapStart on a function
Resources:
ProductApi:
Type: AWS::Serverless::Function
Properties:
Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
Runtime: java21
MemorySize: 1024
SnapStart:
ApplyOn: PublishedVersions # snapshot taken on publish, not $LATEST
AutoPublishAlias: live # always route alias to latest published version
Environment:
Variables:
SPRING_PROFILES_ACTIVE: prod
Uniqueness after restore: After snapshot restore, random seeds, timestamps, and network connections may be stale. Implement CRaC-compatible hooks (@PreSnapshot / @AfterRestore via SnapshotHook) to close and reopen connections. Spring Boot 3.2+ integrates with CRaC automatically for JDBC pools and caches.
AWS SAM (Serverless Application Model) provides a CLI that can invoke Lambda functions locally using Docker, emulate API Gateway, and deploy the full stack via CloudFormation. It is the standard tool for the Lambda development loop.
# Install SAM CLI (macOS)
brew tap aws/tap
brew install aws-sam-cli
# Build the project (compiles, packages, copies to .aws-sam/build/)
sam build
# Start local API Gateway (emulates HTTP triggers on localhost:3000)
sam local start-api
# Invoke a single function with a test event
sam local invoke ProductApi --event events/get-product.json
# Tail logs from a deployed function
sam logs -n ProductApi --stack-name my-stack --tail
# Deploy with guided prompts (first time)
sam deploy --guided
# Deploy using existing samconfig.toml (subsequent deployments)
sam deploy
// events/get-product.json — sample API Gateway v2 event
{
"version": "2.0",
"routeKey": "GET /products/{id}",
"rawPath": "/products/42",
"pathParameters": { "id": "42" },
"requestContext": {
"http": { "method": "GET", "path": "/products/42" },
"stage": "$default"
},
"isBase64Encoded": false
}
Even with SnapStart, understanding cold start causes helps you build faster functions. Cold start time = JVM launch + class loading + Spring context init + first-request logic. Each phase can be optimised independently.
| Technique | Impact | Notes |
| SnapStart | ⬇ 80–95% cold start | Enable via ApplyOn: PublishedVersions; requires Java 11+ managed runtime |
| Increase memory | ⬇ 20–50% init time | More memory = more CPU. 512–1024 MB is the sweet spot for Spring Boot |
| spring.main.lazy-initialization=true | ⬇ 30–60% context init | Beans initialise on first use; trades cold start for slightly slower first request |
| Reduce classpath / dependencies | ⬇ 10–30% class loading | Remove unused Spring Boot starters; use spring-boot-starter-web not spring-boot-starter |
| GraalVM Native Image | ⬇ 95%+ cold start, ⬇ memory | Sub-100 ms starts; requires native build pipeline; no SnapStart needed |
| Provisioned Concurrency | Eliminates cold starts entirely | Pre-warms N instances; costs money even at zero traffic |
# application-lambda.yml — Lambda-optimised Spring Boot settings
spring:
main:
lazy-initialization: true
banner-mode: off
jpa:
open-in-view: false # disable OSIV — saves a database connection per request
datasource:
hikari:
minimum-idle: 1 # cold Lambda doesn't need pool depth
maximum-pool-size: 5
connection-timeout: 3000
Lambda functions read configuration from environment variables. Spring Boot's externalized configuration picks these up automatically — no code changes needed. For secrets, always use AWS Systems Manager Parameter Store or Secrets Manager rather than plain environment variable values.
// application.yml references environment variables
// Spring Boot reads them automatically via @Value or @ConfigurationProperties
spring:
datasource:
url: ${DB_URL} # set in Lambda environment
username: ${DB_USERNAME}
password: ${DB_PASSWORD} # or: resolved from Secrets Manager via AWS extension
cloud:
aws:
region:
static: ${AWS_DEFAULT_REGION:us-east-1}
# Use AWS AppConfig or Parameter Store extension for dynamic config
# Extension reads SSM params and exposes them as localhost HTTP at startup
# SAM template — inject config from SSM Parameter Store
Environment:
Variables:
DB_URL: !Sub "{{resolve:ssm:/myapp/prod/db-url}}"
# Or use the AWS Parameters and Secrets Lambda Extension (layer)
# for runtime fetching without SDK calls in application code
Lambda automatically sends stdout/stderr to CloudWatch Logs. For structured logs and distributed tracing, add the AWS X-Ray SDK and configure Micrometer for CloudWatch metrics. Spring Boot 3's Micrometer integration makes this straightforward.
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-xray-recorder-sdk-spring</artifactId>
<version>2.15.3</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-cloudwatch2</artifactId>
</dependency>
// Structured JSON logging — Lambda CloudWatch Logs Insights can query fields
@Configuration
public class LoggingConfig {
@PostConstruct
void configure() {
// Logback JSON encoder (net.logstash.logback:logstash-logback-encoder)
// Outputs: {"timestamp":"...","level":"INFO","message":"...","traceId":"..."}
}
}
// X-Ray active tracing annotation
@XRayEnabled
@Service
public class ProductService {
public Product findById(Long id) {
// X-Ray creates a subsegment automatically for this method
return repository.findById(id).orElseThrow();
}
}
# Enable X-Ray active tracing in SAM template
Properties:
Tracing: Active
Policies:
- AWSXRayDaemonWriteAccess