Serverless applications on AWS using Lambda with Java 25, API Gateway and DynamoDB - Part 1 Sample application
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:
- Baseline – Deploy the application and measure cold/warm start times without any optimisations.
- Cold‑start reduction – Explore techniques such as Lambda SnapStart, priming, and GraalVM Native Image.
- 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:
| Service | Role |
|---|---|
| Amazon API Gateway | Exposes a REST API for the Lambda functions |
| AWS Lambda | Executes the Java 25 code (or a GraalVM native image) |
| Amazon DynamoDB | NoSQL persistence layer for product data |
| AWS SAM | Declarative IaC for the whole stack |
Assumption: You have a basic understanding of the services above and of serverless architectures on AWS.
Prerequisites
| Tool | Version |
|---|---|
| Java | 25 |
| Maven | latest |
| AWS CLI | latest |
| SAM CLI | latest |
| 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: getPost‑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
idpath parameter, queries DynamoDB viaproductDao.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.
getProductbuilds aGetItemRequest, executes it, and maps the result back to aProductrecord.
Next Steps
- Deploy the SAM stack (
sam build && sam deploy). - Invoke the API endpoints and record cold/warm start times.
- Enable SnapStart (uncomment the
SnapStartblock) and re‑measure. - 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 -gAfter 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/productsRetrieve 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!