DEV Community

Cover image for Serverless applications on AWS using Lambda with Java 25, API Gateway and DynamoDB - Part 1 Sample application
Vadym Kazulkin for AWS Heroes

Posted on • Originally published at vkazulkin.com

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

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
        ...
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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()));
 }
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
Enter fullscreen mode Exit fullscreen mode

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)