Introduction
In this article series, we'll explain how to implement a serverless application on AWS using Lambda with the support of the released Java 25 version. We'll also use API Gateway, DynamoDB, and AWS SAM for the Infrastructure as Code. After it, we'll measure the performance (cold and warm start times) of the Lambda function without any optimizations. Hereafter, we'll introduce various cold start time reduction approaches like Lambda SnapStart with priming techniques and GraalVM Native Image. In this article, we'll introduce our sample application.
Sample application and its architecture
You can find a code example of our sample application in my GitHub aws-lambda-java-25-dynamodb.
The architecture of our sample application is shown below:
In this application, we will create products and retrieve them by their ID and use Amazon DynamoDB as a NoSQL database for the persistence layer. We use Amazon API Gateway, which makes it easy for developers to create, publish, maintain, monitor, and secure APIs. Of course, we rely on AWS Lambda to execute code without the need to provision or manage servers. We also use AWS SAM, which provides a short syntax optimised for defining infrastructure as code (hereafter IaC) for serverless applications. For this article, I assume a basic understanding of the mentioned AWS services, serverless architectures on AWS, and AWS SAM. The application is intentionally fairly simple. The goal is to demonstrate the general development concepts and cover approaches to reduce the cold start time of the Lambda. Please also watch out for another series where I use relational serverless Amazon Aurora DSQL database and additionally Hibernate ORM framework instead of DynamoDB to do the same Lambda performance measurements.
To build and deploy the sample application, we need the following local installations: Java 25, Maven, AWS CLI, and SAM CLI. Later, we'll also need GraalVM, including its Native Image capabilities. Using it, we'll build a native image of our application to deploy it on AWS Lambda using the Custom Runtime.
Let's start by covering the IaC part described in AWS SAM template.yaml. We'll focus only on the parts relevant to the definitions of the Lambda functions there.
In the global section, we define the common properties valid for all defined Lambda functions. To such properties belong code URI, runtime (in our case Java 25), Snapstart usage yes/no, timeout, memory size, and environment variables:
Globals:
Function:
CodeUri: ....
Runtime: java25
#SnapStart:
#ApplyOn: PublishedVersions
Timeout: 30
MemorySize: 1024
Architectures:
- x86_64
Environment:
Variables:
REGION: !Sub ${AWS::Region}
PRODUCT_TABLE_NAME: !Ref ProductsTable
...
Below is an example of the definition of the Lambda function with the name GetProductByIdJava25WithDynamoDB. We define the handler: a Java class and method that will be invoked. We also give this Lambda function read access to the DynamoDB table with the name ProductsTable. At the end, we define the event to invoke this particular Lambda function. As we use a REST application and API Gateway in front, we define the HTTP method get and the path /products/{id} for it. This means that the invocation of this Lambda function occurs when an HTTP GET request comes in to retrieve the product by its id.
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
The definition of another Lambda function PostProductJava25WithDynamoDB is similar.
Now let's look at the source code of the GetProductByIdHandler Lambda function that will be invoked when the Lambda function with the name GetProductByIdJava25WithDynamoDB gets invoked. This Lambda function determines the product based on its ID and returns it:
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) {
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 only method handleRequest receives an object of type APIGatewayProxyRequestEvent as input, as APIGatewayRequest invokes the Lambda function. From this input object, we retrieve the product ID by invoking requestEvent.getPathParameters().get("id"). Then we ask our ProductDao to find the product with this ID in the DynamoDB by invoking productDao.getProduct(id). Depending on whether the product exists or not, we wrap the Jackson serialised response in an object of type APIGatewayProxyResponseEvent and send it back to Amazon API Gateway as a response. The source code of the Lambda function CreateProductHandler, which we use to create and persist products, looks similar.
The source code of the Product entity looks very simple:
public record Product(String id, String name, BigDecimal price) {}
The implementation of the ProductDao persistence layer uses AWS SDK for Java 2.0 to write to or read from the DynamoDB. Here is an example of the source code of the getProductById method, which we used in the GetProductByIdHandler Lambda function described above:
public Optional<Product> getProduct(String id) {
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 the instance of the DynamoDbClient to build a GetItemRequest to query the DynamoDB table. We get the name of the table from an environment variable (which we will set in the AWS SAM template) by invoking System.getenv("PRODUCT_TABLE_NAME"), for the product based on its ID. If the product is found, we use the custom-written ProductMapper to map the DynamoDB item to the attributes of the product entity.
Now we have to build the application with mvn clean package and deploy it with sam deploy -g. We will see our customised Amazon API Gateway URL in the return. We can use it to create products and retrieve them by ID. The interface is secured with the API key. We have to send the following as an HTTP header: "X-API-Key: a6ZbcDefQW12BN56WEVDDB25", see MyApiKey definition in template.yaml. To create the product with ID=1, we can use the following curl query:
curl -m PUT -d '{ "id": 1, "name": "Print 10x13", "price": 0.15 }' -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25" https://{$API_GATEWAY_URL}/prod/products
For example, to query the existing product with ID=1, we can use the following curl query:
curl -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25" https://{$API_GATEWAY_URL}/prod/products/1
Conclusion
In this article, we introduced our sample application. In the next article, we'll measure the performance (cold and warm start times) of the Lambda function without any optimizations.
Please also watch out for another series where I use relational serverless Amazon Aurora DSQL database and additionally Hibernate ORM framework instead of DynamoDB to do the same Lambda performance measurements.
If you like my content, please follow me on GitHub and give my repositories a star!
Please also check out my website for more technical content and upcoming public speaking activities.

Top comments (0)