Java is Back on Lambda: Building a Sub-Second GenAI API with Spring Boot 3, SnapStart, and Bedrock
Source: Dev.to

Is Java too slow for AWS Lambda? For years, the answer was “yes, mostly” due to the dreaded cold starts. Today, with Java 21 and SnapStart, the answer is “absolutely not”.
In this post I show how I built a production‑grade serverless API using Spring Boot 3, Java 21, and AWS Bedrock (Claude 3.5) that starts in under 500 ms.
The Problem: The “Cold Start” Tax
If you’ve run Java on Lambda before, you know the pain. The JVM is heavy; loading classes, initializing the Spring context, and setting up AWS SDKs can take 5 to 15 seconds.
- Asynchronous background jobs can tolerate this.
- Synchronous APIs (e.g., chatbots or REST endpoints) cannot.
The Solution: AWS Lambda SnapStart
SnapStart changes the game by using CRaC (Coordinated Restore at Checkpoint).
- AWS starts your function during the deployment phase.
- It runs the full initialization (JVM warm‑up, Spring context, dependency injection).
- It takes a memory snapshot of the initialized Firecracker microVM.
- It caches this snapshot.
When a user invokes the API, Lambda simply restores the memory state—like waking a laptop from hibernation rather than booting it cold.
The Architecture
The project integrates Generative AI (AWS Bedrock) via a Spring Cloud Function.
Key components
| Component | Detail |
|---|---|
| Runtime | Java 21 (AWS Corretto) |
| Framework | Spring Boot 3.2 + Spring Cloud Function |
| Infrastructure | Terraform |
| AI Model | Anthropic Claude 3.5 Sonnet (via AWS Bedrock) |
The Code: Optimization Techniques
1. Smart Initialization (Constructor Injection)
Heavy work (creating the Bedrock client) is moved to the constructor, so SnapStart captures it during deployment, not during invocation.
@Service
public class BedrockService {
private final BedrockRuntimeClient bedrockClient;
public BedrockService() {
// CRITICAL: runs during the "Deployment" phase, not the "Invocation" phase!
this.bedrockClient = BedrockRuntimeClient.builder()
.region(Region.US_EAST_1)
// Use the lightweight HTTP client instead of Netty
.httpClient(UrlConnectionHttpClient.builder().build())
.build();
}
// ... business logic ...
}
Pro tip: Replacing the default Netty HTTP client with UrlConnectionHttpClient reduces artifact size and start‑up time, which is critical for Lambda performance.
2. Terraform Configuration
Enabling SnapStart is a one‑liner, but you must also enable version publishing.
resource "aws_lambda_function" "java_snapstart_function" {
function_name = "java-bedrock-poc"
runtime = "java21"
handler = "org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest"
# ... other config ...
publish = true # REQUIRED for SnapStart
snap_start {
apply_on = "PublishedVersions"
}
environment {
variables = {
# Tune JVM for fast tier‑1 compilation
JAVA_TOOL_OPTIONS = "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
}
}
}
The Benchmark: Did It Work?
I deployed the function and tested it with the AWS CLI. The difference is night and day.
| Metric | Without SnapStart | With SnapStart 🚀 |
|---|---|---|
| Init Duration | ~8,000 ms | 0 ms (cached) |
| Restore Duration | N/A | ~350 ms |
| Execution | ~1,500 ms | ~1,500 ms |
| Total User Wait | ~9.5 seconds 🐢 | ~1.8 seconds 🚀 |
Note: Execution time includes the call to Claude 3.5 (GenAI). The Java Lambda overhead dropped to sub‑second levels.
Conclusion
Java is no longer a second‑class citizen in the serverless world. By combining Spring Boot 3, SnapStart, and lightweight clients, we can build enterprise‑grade, strongly typed, and testable applications that perform as well as Node.js or Python.
For senior architects handling legacy migrations, this provides a viable path to move complex monoliths to AWS without rewriting everything in a new language.
Source Code
The full working example (including Terraform code and specialized shell scripts) is available on GitHub: