Cloud (AWS / Azure)15 min read·By Liyabona Saki·

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.

Advertisement

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

text
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

xml
<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

xml
<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:compile

FROM 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

yaml
# 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: .
bash
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-time for 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:

java
@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

CLIENTAPI GATEWAYCOMPUTEDATAHTTPSinvokeread / writepublishClientAPI GatewayREST / HTTPLambdaSpring Boot · GraalVM nativeDynamoDBSQSevents queue
API Gateway invokes a Lambda packaged with a GraalVM native image for fast cold starts. The function reads from DynamoDB and writes events to SQS.

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

#AWS Lambda#Serverless#GraalVM#Spring Boot#Native Image

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