Serverless applications on AWS using Lambda with Java 25, API Gateway and DynamoDB - Part 1 Sample application

Published: (March 16, 2026 at 11:31 AM EDT)
5 min read
Source: Dev.to

Source: Dev.to

In this article series we’ll explain how to implement a server‑less application on AWS using Lambda with the newly released Java 25 runtime.
We’ll also use API Gateway, DynamoDB, and AWS SAM for the infrastructure‑as‑code (IaC) definition.

The series is divided into three parts:

  1. Baseline – Deploy the application and measure cold/warm start times without any optimisations.
  2. Cold‑start reduction – Explore techniques such as Lambda SnapStart, priming, and GraalVM Native Image.
  3. Extended example – A companion series that swaps DynamoDB for a relational serverless Amazon Aurora DSQL database and uses Hibernate ORM.

You can find the sample code on GitHub: aws‑lambda‑java‑25‑dynamodb.

Architecture Overview

The sample application provides a simple product catalogue:

  • Create a product (POST)
  • Retrieve a product by its ID (GET)

It uses the following AWS services:

ServiceRole
Amazon API GatewayExposes a REST API for the Lambda functions
AWS LambdaExecutes the Java 25 code (or a GraalVM native image)
Amazon DynamoDBNoSQL persistence layer for product data
AWS SAMDeclarative IaC for the whole stack

Assumption: You have a basic understanding of the services above and of serverless architectures on AWS.

Prerequisites

ToolVersion
Java25
Mavenlatest
AWS CLIlatest
SAM CLIlatest
GraalVM (optional, for native image)latest with native-image component

We will first build and deploy the Java 25 version, then later compile a GraalVM native image and run it with a custom runtime.

AWS SAM Template – IaC Highlights

Below are the relevant parts of template.yaml. Only the sections that affect the Lambda functions are shown.

Global Function Settings

Globals:
  Function:
    CodeUri: .
    Runtime: java25
    # SnapStart (uncomment to enable)
    # SnapStart:
    #   ApplyOn: PublishedVersions
    Timeout: 30
    MemorySize: 1024
    Architectures:
      - x86_64
    Environment:
      Variables:
        REGION: !Sub ${AWS::Region}
        PRODUCT_TABLE_NAME: !Ref ProductsTable
        # … other variables …

Get‑Product‑By‑Id Function

GetProductByIdFunction:
  Type: AWS::Serverless::Function
  Properties:
    FunctionName: GetProductByIdJava25WithDynamoDB
    AutoPublishAlias: liveVersion
    Handler: software.amazonaws.example.product.handler.GetProductByIdHandler::handleRequest
    Policies:
      - DynamoDBReadPolicy:
          TableName: !Ref ProductsTable
    Events:
      GetRequestById:
        Type: Api
        Properties:
          RestApiId: !Ref MyApi
          Path: /products/{id}
          Method: get

Post‑Product Function

The definition of PostProductJava25WithDynamoDB follows the same pattern (omitted for brevity).

Lambda Handler – GetProductByIdHandler

@Override
public APIGatewayProxyResponseEvent handleRequest(
        APIGatewayProxyRequestEvent requestEvent,
        Context context) throws JsonProcessingException {

    var id = requestEvent.getPathParameters().get("id");
    var optionalProduct = productDao.getProduct(id);

    if (optionalProduct.isEmpty()) {
        return new APIGatewayProxyResponseEvent()
                .withStatusCode(HttpStatusCode.NOT_FOUND)
                .withBody("Product with id = " + id + " not found");
    }

    return new APIGatewayProxyResponseEvent()
            .withStatusCode(HttpStatusCode.OK)
            .withBody(objectMapper.writeValueAsString(optionalProduct.get()));
}
  • The method receives an APIGatewayProxyRequestEvent (invoked by API Gateway).
  • It extracts the id path parameter, queries DynamoDB via productDao.getProduct(id), and returns either a 404 or 200 response with a JSON payload.

The CreateProductHandler (used for POST) follows the same structure.

Domain Model – Product Entity

public record Product(String id, String name, BigDecimal price) {}

A simple immutable record that represents a product.

Persistence Layer – ProductDao (excerpt)

public class ProductDao {

    private final DynamoDbClient dynamoDb;
    private final String tableName;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public ProductDao(DynamoDbClient dynamoDb, String tableName) {
        this.dynamoDb = dynamoDb;
        this.tableName = tableName;
    }

    public Optional getProduct(String id) {
        GetItemResponse response = dynamoDb.getItem(
                GetItemRequest.builder()
                        .tableName(tableName)
                        .key(Map.of("id", AttributeValue.builder().s(id).build()))
                        .build());

        if (response.hasItem()) {
            var item = response.item();
            var product = objectMapper.convertValue(item, Product.class);
            return Optional.of(product);
        }
        return Optional.empty();
    }

    // … methods for putItem, deleteItem, etc. …
}
  • Uses AWS SDK for Java 2.x to interact with DynamoDB.
  • getProduct builds a GetItemRequest, executes it, and maps the result back to a Product record.

Next Steps

  1. Deploy the SAM stack (sam build && sam deploy).
  2. Invoke the API endpoints and record cold/warm start times.
  3. Enable SnapStart (uncomment the SnapStart block) and re‑measure.
  4. Build a GraalVM native image and deploy it as a custom runtime for further cold‑start reduction.

Stay tuned for the upcoming articles where we dive deeper into each optimisation technique!

Retrieve a Product from DynamoDB

GetItemResponse getItemResponse = dynamoDbClient.getItem(
    GetItemRequest.builder()
        .key(Map.of("PK", AttributeValue.builder().s(id).build()))
        .tableName(PRODUCT_TABLE_NAME)
        .build());

if (getItemResponse.hasItem()) {
    return Optional.of(ProductMapper.productFromDynamoDB(getItemResponse.item()));
} else {
    return Optional.empty();
}

Here we use an instance of DynamoDbClient to build a GetItemRequest that queries the DynamoDB table.
The table name is obtained from an environment variable (set in the AWS SAM template) via System.getenv("PRODUCT_TABLE_NAME").
If the product is found, the custom ProductMapper converts the DynamoDB item into a Product entity.

Build & Deploy

# Build the application
mvn clean package

# Deploy with SAM (guided mode)
sam deploy -g

After deployment, SAM returns a customised Amazon API Gateway URL. This URL is used to create and retrieve products. The API is secured with an API key, which must be sent in the X-API-Key header (see MyApiKey definition in template.yaml).

API key: a6ZbcDefQW12BN56WEVDDB25

Example Requests

Create a product (ID = 1)

curl -X PUT \
     -d '{ "id": 1, "name": "Print 10x13", "price": 0.15 }' \
     -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25" \
     https://{API_GATEWAY_URL}/prod/products

Retrieve a product (ID = 1)

curl -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25" \
     https://{API_GATEWAY_URL}/prod/products/1

(Replace {API_GATEWAY_URL} with the URL returned by sam deploy.)

What’s Next?

In this article we introduced the sample application. In the next article we’ll measure cold‑ and warm‑start times of the Lambda function without any optimisations.

Later we’ll use a relational, serverless Amazon Aurora (PostgreSQL) database together with Hibernate ORM to perform the same Lambda performance measurements.

If you enjoy the content, please:

  • Star my repositories on GitHub
  • Follow me for more technical articles and upcoming public‑speaking events
  • Visit my personal website for additional resources

Thank you!

0 views
Back to Blog

Related posts

Read more »

Travigo

Travel as fast as you speak with Gemini! Where live agents meet immersive storytelling & 3D navigation. This project was created for entering the Gemini Live Ag...

Micro games

Hey Gamers! 👾 As part of the Rapid Games Prototyping module, we are tasked with reviewing a peer's game. The challenge is to analyse a prototype built in just...