DEV Community

Cover image for Java is Back on Lambda: Building a Sub-Second GenAI API with Spring Boot 3, SnapStart, and Bedrock
Jacques Montagne
Jacques Montagne

Posted on

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

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 will show you how I built a production-grade Serverless API using Spring Boot 3, Java 21, and AWS Bedrock (Claude 3.5) that starts in under 500ms.

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.

For an asynchronous background job, this is fine. For a synchronous API (like a chatbot or REST endpoint), this is unacceptable.

The Solution: AWS Lambda SnapStart

SnapStart changes the game by using CRaC (Coordinated Restore at Checkpoint).

Instead of initializing from scratch every time, the process looks like this:

  1. AWS starts your function during the deployment phase.
  2. It runs the full initialization (JVM warmup, Spring Context, Dependency Injection).
  3. It takes a memory snapshot of the initialized Firecracker microVM.
  4. It caches this snapshot.

When a user invokes your API, Lambda simply restores the memory state. It's like waking up a laptop from hibernation rather than booting it cold.

The Architecture

I built a project that integrates Generative AI (AWS Bedrock) via a Spring Cloud Function. (See the cover image for the full Sequence Diagram).

Key components:

  • 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

To make this work efficiently, I didn't just turn on SnapStart. I optimized the code structure to maximize the benefit of the snapshotting process.

1. Smart Initialization (Constructor Injection)

I moved the heavy lifting (creating the Bedrock Client) to the constructor. Because SnapStart creates the snapshot after initialization, this heavy cost is paid only once during deployment, never by the user.

@Service
public class BedrockService {
    private final BedrockRuntimeClient bedrockClient;

    public BedrockService() {
        // CRITICAL: This 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 ...
}
Enter fullscreen mode Exit fullscreen mode

Pro Tip: I replaced the default Netty HTTP Client with UrlConnectionHttpClient. It creates a much smaller artifact and starts faster, which is critical for Lambda performance.

2. Terraform Configuration

Enabling SnapStart is a one-liner in Terraform, but there is a catch: you must 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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Benchmark: Did it work?

I deployed the function and ran tests using 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: The execution time includes the call to Claude 3.5 (GenAI), which takes time to generate text. The actual overhead of the Java Lambda itself 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 just as well as Node.js or Python.

For a Senior Architect dealing with legacy migration, this is the missing link to moving complex monoliths to AWS without rewriting everything in a new language.

๐Ÿ”— Source Code

Check out the full working example (including Terraform code and specialized shell scripts) on my GitHub:

github.com/jmontagne/poc_java_lambda_snapstart_bedrock


Let me know in the comments: Have you tried Java 21 on Lambda yet?

Top comments (0)