Serverless Java: Spring Boot on AWS Lambda with GraalVM
Deploy Spring Boot on AWS Lambda with GraalVM native image to eliminate cold starts — including build configuration, API Gateway integration, benchmarks and cost comparisons vs containers.
Introduction
Java on AWS Lambda has a reputation problem: cold starts of 3–10 seconds make it unusable for latency-sensitive APIs. GraalVM native image changes that — a Spring Boot app compiled ahead of time starts in under 200ms and uses a fraction of the memory.
This tutorial walks through building, deploying and benchmarking a Spring Boot Lambda with GraalVM.
Serverless architecture concepts
client ──► API Gateway ──► Lambda (Spring Boot native) ──► DynamoDB / RDS
You pay only for invocations and execution time. Scaling is automatic — Lambda spins up new containers on demand and tears them down when idle.
The cold start problem
A Lambda "cold start" is the time from invocation to your handler returning. For JVM Spring Boot:
| Stage | JVM Spring Boot | Native (GraalVM) | | ------------------ | --------------- | ---------------- | | Init (class load) | ~3000 ms | ~80 ms | | First invocation | ~500 ms | ~30 ms | | Memory used | 512–1024 MB | 128–256 MB |
The JVM cold start hurts every time Lambda scales up — exactly when you need speed.
Step 1 — Spring Boot project with Lambda adapter
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.11.4</version>
</dependency>
```java
@SpringBootApplication
public class OrdersApp {
public static void main(String[] args) { SpringApplication.run(OrdersApp.class, args); }@Bean public Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> getOrder(OrderService svc) { return event -> { String id = event.getPathParameters().get("id"); Order o = svc.find(id); return new APIGatewayProxyResponseEvent() .withStatusCode(200) .withBody(toJson(o)); }; } } ```
Step 2 — GraalVM native build
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<arg>--no-fallback</arg>
<arg>-H:+ReportExceptionStackTraces</arg>
</buildArgs>
</configuration>
</plugin>
Build inside the official Amazon Linux 2023 + GraalVM container so the binary is compatible with the Lambda runtime:
```dockerfile
FROM ghcr.io/graalvm/native-image-community:21 AS build
WORKDIR /src
COPY . .
RUN ./mvnw -B -Pnative native:compileFROM public.ecr.aws/lambda/provided:al2023 COPY --from=build /src/target/orders /var/runtime/bootstrap RUN chmod +x /var/runtime/bootstrap CMD ["orders.OrdersApp"] ```
Step 3 — Deploy with AWS SAM
# template.yaml
Transform: AWS::Serverless-2016-10-31
Resources:
OrdersFn:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
MemorySize: 256
Timeout: 10
Architectures: [arm64]
Events:
Api:
Type: HttpApi
Properties: { Path: /orders/{id}, Method: GET }
Metadata:
Dockerfile: Dockerfile
DockerContext: .
sam build && sam deploy --guided
Step 4 — Cold start benchmarks
Measured on arm64 with 256 MB memory:
| Variant | p50 cold | p99 cold | p50 warm | | ------------------------ | -------- | -------- | -------- | | Spring Boot JVM (Java 21)| 3200 ms | 6100 ms | 12 ms | | Spring Boot Native | 180 ms | 320 ms | 8 ms | | Plain Java handler | 350 ms | 720 ms | 5 ms |
Native is 17–19× faster on cold start than the JVM build.
Step 5 — Cost comparison vs containers
Take a low-traffic API: 1M requests/month, 100ms avg, 256MB.
| Platform | Monthly cost | | ----------------------------------- | ------------ | | Lambda native (arm64, 256MB) | ~$2.30 | | ECS Fargate (0.25 vCPU, 0.5GB, 24/7)| ~$10.80 | | EKS (smallest node, shared) | ~$73 + share |
Lambda wins at low/spiky volume; containers win once steady-state CPU goes above ~30%.
Optimization tips
- Build with
--initialize-at-build-timefor stable libraries — moves more work out of cold start. - Use SnapStart (JVM) if you can't migrate to native — also cuts cold starts to ~200ms.
- Keep dependencies small: every JAR is more reflection metadata GraalVM has to track.
- Reuse SDK clients in static fields — Lambda freezes them between invocations.
API Gateway integration
For HTTP APIs, use the payload v2.0 format — smaller and faster than the REST API v1 format. Map path/query/body in your handler:
@Bean
public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> handler(...) { ... }
Real-world use cases
- Public REST APIs with low or unpredictable traffic.
- Webhook receivers (Stripe, GitHub, Slack).
- Event processors triggered by SQS, S3, EventBridge.
- GraphQL resolvers behind AppSync.
- Scheduled jobs (EventBridge cron) replacing cron-on-EC2.
Caveats
- Reflection-heavy libraries (Hibernate, some serializers) need native hints — supply via
@RegisterReflectionForBinding. - Native build is slow — 3–6 minutes. Cache aggressively in CI.
- Debugging differs — no live JIT introspection; rely on logs and AWS X-Ray.
Related tutorials
Architecture
Serverless Spring Boot on AWS Lambda
TL;DR
Key takeaways
- Understand the core concepts behind Serverless Java Spring Boot on AWS Lambda with GraalVM in a production context.
- Apply the patterns to real Cloud (AWS / Azure) systems, not just toy examples.
- Recognize the trade-offs, failure modes, and operational concerns before adopting them.
- Get a clear path to the next step — related tutorials, tools, and reference architectures.
Avoid these
Common mistakes
1. Copy-pasting code without understanding the trade-offs
It's tempting to ship a snippet from a blog post into production, but Cloud (AWS / Azure) patterns only work when the failure modes are understood. Always reason about timeouts, retries, and consistency.
2. Skipping observability from day one
Structured logs, metrics, and traces are not optional. Wire them in before you ship — debugging Cloud (AWS / Azure) systems without them is painful and expensive.
3. Optimizing too early
Premature caching, sharding, or microservice extraction adds operational cost. Validate the bottleneck with real measurements first.
4. Ignoring security defaults
Secrets in env files, open management ports, missing RBAC — these are the most common production incidents. Treat security as part of the definition of done.
Ship it safely
Production best practices
Apply these before promoting Serverless Java: Spring Boot on AWS Lambda with GraalVM to a real production environment.
Scalability
Design Cloud (AWS / Azure) services to scale horizontally. Keep request handlers stateless, push session and cache state to external stores (Redis, the database), and benchmark p95/p99 latency under realistic load before tuning.
Monitoring & Observability
Emit metrics (RED/USE), structured JSON logs, and distributed traces from day one. Wire dashboards and alerts to SLOs you actually care about — error rate, latency, saturation — not vanity metrics.
Logging
Log with correlation IDs, never log secrets or PII, and centralize logs (ELK, Loki, CloudWatch). Use levels deliberately: INFO for state changes, WARN for recoverable issues, ERROR for incidents.
Security
Apply least-privilege IAM, rotate secrets through a vault, validate every input, and patch dependencies on a schedule. For HTTP services, enable TLS everywhere and set sensible security headers.
Testing
Layer unit, integration, and contract tests. Run them in CI on every PR, and add smoke tests post-deploy. For Cloud (AWS / Azure) systems, also run chaos and load tests before a major release.
Reliability & Rollouts
Ship with health checks, readiness probes, graceful shutdown, and a rollback strategy. Prefer canary or blue/green deploys over big-bang releases.
Questions
Frequently asked questions
Is this tutorial up to date?
Yes. This tutorial was last reviewed and updated on May 23, 2026. We revisit popular Cloud (AWS / Azure) tutorials regularly to keep them aligned with current best practices.
What level is this tutorial aimed at?
It is written for working developers with some backend experience. Beginners can still follow along, and senior engineers will find production-grade patterns and trade-off discussions.
Do I need to follow every step in order?
The walkthrough is sequential because each step depends on the previous one. If you only need a specific concept, the table of contents at the top of the article lets you jump straight to that section.
Where can I find the source code?
Code samples are inlined in the tutorial. When a companion repository is published it will be linked at the top of this page.
Go deeper
Further reading
More From the Channel
Follow the full tutorial series on YouTube
The MasterLabSystems channel publishes in-depth, project-based tutorials on Java, Spring Boot, microservices, Docker, Kubernetes, AWS and DevOps — the same topics covered on this site, with full code walkthroughs.
Stay in the Loop
Get the next tutorial in your inbox
next tutorial →
Implementing Distributed Locking with Java Spring Boot and Redis for Data Integrity
Related tutorials
Deploying Spring Boot to AWS: ECS Fargate End-to-End
Containerize a Spring Boot app, push to ECR, run on ECS Fargate behind an Application Load Balancer — production-ready in one tutorial.
Deploy Spring Boot to AWS ECS Fargate — End to End
Containerize a Spring Boot service and ship it to AWS ECS Fargate behind an Application Load Balancer.
Spring Boot Microservices Architecture Explained Step by Step
A complete, beginner-friendly walkthrough of microservices architecture using Spring Boot — services, gateway, discovery, config and observability.
