Spring Boot on AWS Lambda: Serverless Java with SnapStart

Running Spring Boot on AWS Lambda combines the familiar Spring programming model with the cost and scaling benefits of serverless. The challenge has always been cold starts: the time it takes for the JVM to initialise and Spring to wire its context before the first request. AWS SnapStart (2022) addresses this by taking a Firecracker snapshot of the initialised JVM, reducing cold starts to under 1 second. This article covers two deployment approaches: the Lambda Web Adapter (treats Lambda as an HTTP server) and the native function handler (for lightweight operations), plus SAM CLI local testing and tuning strategies.

Share

Contents

Deployment Approaches

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.

ApproachHow it worksBest for
Lambda Web AdapterA thin Lambda extension that proxies API Gateway events to an HTTP server running on localhost:8080 inside the same execution environmentMigrating existing Spring Boot REST APIs; familiar MVC/WebFlux code with no changes
Function HandlerImplement RequestHandler<I,O> or Spring Cloud Function's Function<I,O>; Spring context starts on first invocationLightweight 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.

Lambda Web Adapter: Full Spring Boot

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.

XML
<!-- pom.xml — standard Spring Boot Web dependency; no Lambda SDK needed -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Java
// 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
# 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"]

Native Function Handler

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.

XML
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
Java
@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());
        };
    }
}
YAML
# application.yml
spring:
  cloud:
    function:
      definition: processOrder   # tells the AWS adapter which Function bean to invoke
Text
# Lambda configuration
Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
Runtime: java21
Memory: 512 MB
Timeout: 30 s

AWS SnapStart

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
YAML
# 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.

SAM CLI: Local Testing & Deployment

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.

Shell
# 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
JSON
// 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
}

Cold Start Optimisation

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.

TechniqueImpactNotes
SnapStart⬇ 80–95% cold startEnable via ApplyOn: PublishedVersions; requires Java 11+ managed runtime
Increase memory⬇ 20–50% init timeMore memory = more CPU. 512–1024 MB is the sweet spot for Spring Boot
spring.main.lazy-initialization=true⬇ 30–60% context initBeans initialise on first use; trades cold start for slightly slower first request
Reduce classpath / dependencies⬇ 10–30% class loadingRemove unused Spring Boot starters; use spring-boot-starter-web not spring-boot-starter
GraalVM Native Image⬇ 95%+ cold start, ⬇ memorySub-100 ms starts; requires native build pipeline; no SnapStart needed
Provisioned ConcurrencyEliminates cold starts entirelyPre-warms N instances; costs money even at zero traffic
YAML
# 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

Configuration & Environment Variables

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.

Java
// application.yml references environment variables
// Spring Boot reads them automatically via @Value or @ConfigurationProperties
YAML
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
YAML
# 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

Observability: Logs, Tracing & Metrics

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.

XML
<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>
Java
// 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();
    }
}
YAML
# Enable X-Ray active tracing in SAM template
Properties:
  Tracing: Active
  Policies:
    - AWSXRayDaemonWriteAccess