Building a serverless architecture with Java and Quarkus

16/09/2021
Updated 06/11/2026
Dennis Baerten
Title slide of the talk Lambda with Java & Quarkus
Creating a serverless architecture using Java & Quarkus, AWS User Group UK. The video comes from YouTube and only loads when you press play.

My talk at the AWS User Group UK

At the AWS User Group UK I recently talked about how we build a serverless SaaS application in Java at Ennovative Solutions, and how Quarkus helps us keep the cold starts of our Lambda functions under control.

In this post I sum up the key points. You can watch the full recording here: Creating a serverless architecture using Java & Quarkus.

Update November 2026: at the bottom of this post you'll find what has changed since and what I would do differently today.

The context: a SaaS product, fully serverless

For a SaaS product, serverless is a logical choice. You pay per request instead of per running server, every customer scales automatically, and there is no infrastructure to patch or monitor at the OS level.

Our architecture rests on the classic AWS building blocks: API Gateway as the front door, Lambda functions for the business logic and managed services for storage and events. Each function does one thing and can be deployed and scaled separately.

Choosing Java is the obvious move: it's the language our team is strongest in, with a mature ecosystem and tooling. Java on Lambda just has one well-known weak spot.

Concretely, this is PURQR, our platform that lets retailers manage their showroom through QR codes: in-store analytics, appointments and a personalised follow-up after the visit. The backend consists of seven Quarkus microservices, most of them HTTP-based behind API Gateway, with SQS for asynchronous emails and DynamoDB for storage. They have been running in production since October 2019.

The problem: Java and cold starts

A cold start is the first invocation of a Lambda function for which no warm execution environment is ready. AWS then has to set up a new environment, load your code and initialise the runtime.

For Java, that has traditionally been expensive. The JVM has to start, load and verify classes, and frameworks like Spring do a lot of work at startup through reflection and classpath scanning. A cold start of several seconds is no exception.

For a SaaS product that is noticeable: the user who happens to hit that cold function has to wait. And in an architecture with many small functions it happens more often than you'd think, especially with low traffic or right after a new deploy.

Our approach: Quarkus and native images

Quarkus tackles the problem at its source: as much work as possible happens at build time instead of at startup. Configuration, dependency injection and annotations are processed up front, leaving far less to do during a cold start.

The real gain comes from the native build. With GraalVM, Quarkus compiles your application ahead of time into a native binary. It runs without a JVM, starts in milliseconds and uses significantly less memory, which on Lambda translates directly into lower costs.

This is what the setup looks like:

  1. The quarkus-amazon-lambda extension connects your handler to the Lambda runtime.
  2. The native build runs in a container, so the binary is compatible with Lambda's Linux environment.
  3. The binary is deployed as a custom runtime (provided), with a bootstrap file as its entry point.

That comes at a price. Native builds take minutes instead of seconds, libraries that rely on reflection need extra configuration, and not every library works out of the box in a native image. That's why debugging is best done in JVM mode, with the native build as a step in the pipeline.

Wrapping up

Quarkus makes Java a first-class choice for serverless. The native build adds some extra work to your pipeline, but the gains in startup time and memory usage are more than worth it.

Are you building a serverless architecture in Java yourself, or weighing native against SnapStart? At Ennovative Solutions we're happy to think along with you. Feel free to get in touch.

Update November 2026: what has changed since

The biggest change: AWS has tackled the Java cold-start problem itself. A native build is no longer the only way to get fast Java functions.

DateWhat changedWhy it matters
2026provided.al2 deprecated, succeeded by provided.al2023 (supported until June 2029)Native Quarkus functions on the old custom runtime need to migrate
November 2025Java 25 runtime on Lambda, with AOT caches replacing Class Data SharingFaster cold starts on the plain JVM, even without SnapStart
November 2022Lambda SnapStart launched for Java 11Cold starts up to ten times faster, without a native build

SnapStart in a nutshell

With SnapStart, Lambda initialises your function when you publish a version, takes a snapshot of that started environment and resumes new instances from that snapshot. Today it works for Java 11 and later, Python 3.12+ and .NET 8+, and for Java there is no extra charge.

There are conditions. SnapStart only works on published versions, not on $LATEST, and not on custom runtimes or container images. A native Quarkus binary and SnapStart are therefore mutually exclusive.

Quarkus has caught up

Quarkus 3 has built-in SnapStart optimisations in its Lambda extension, enabled by default. Classes are preloaded before the snapshot, and through the CRaC API you can initialise clients such as the DynamoDB client before the snapshot is taken (priming). That takes another big bite out of the first invocation.

Update: what I would do differently today

Today I would start with Quarkus on the managed Java runtime with SnapStart, and only reach for a native build when measurements justify it. Quarkus remains an excellent choice; what changes is the default way to deploy it.

ApproachCold startBuild and maintenanceWhen to choose
Quarkus + JVM + SnapStartShort, especially with primingRegular JVM build, managed runtime patched by AWSThe default for most APIs and background jobs
Quarkus native (provided.al2023)Shortest, plus low memory useSlow builds, reflection configuration, maintaining the runtime yourselfHigh volumes where memory and cost per invocation matter
Quarkus + JVM without SnapStartLongest, but better thanks to Java 25SimplestAsynchronous functions where latency doesn't matter

Three lessons I take with me:

  • Measure first. Cold starts often affect only a small share of invocations. Look at your own numbers before adding complexity.
  • Use the managed runtime where you can. AWS patches the JVM and the OS for you; with a custom runtime that's your responsibility, as the provided.al2 migration showed.
  • Design for snapshots. Unique values, network connections and credentials created during initialisation can be reused after a restore. Use the CRaC hooks to refresh them.

Sources