Micronaut JPA Application Performance on AWS Lambda

In this article, we would be looking into how we can deploy a Micronaut application providing GET, PUT and POST which can be called using an API Gateway. Then we would compare its performance when deployed with JVM runtime and as a native image.

Introduction

I have previously written an article here which will help you to create a Micronaut function handler from scratch. I would recommend you to read it if you want to start from the beginning. If you want to skip it, you can use the code directly from my Github repo.

With this let’s get started.

Code Modifications

We can apply a simple MVC pattern, wherein we can create a controller, a service that has some business logic and does the transformation from a DTO to a model, and then a repository to persist it. This pattern is totally optional to implement. You can also apply a Domain-Driven Design(DDD) pattern to structure your code. If you would like to see a simple DDD code structure pattern using Micronaut you can have a look at this GitHub repo.

But we will keep things simple and call the repository directly from the controller, similar to our previous example wherein we called the repository from the handler function.

So the first change we will do is delete two classes.

  • OrderLambdaRuntime
  • OrderRequestHandler

With this, we will be left with only the model and repository.

Now, let's create our controller to handle the incoming request. I will create two methods, PUT to add orders and GET to get all the orders.

@Validated
@Controller
public class WebController {

final private OrderRepository orderRepository;

public WebController(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

@Get("/{id}")
public HttpResponse<Order> getOrder(@PathVariable("id") Long id) {

Optional<Order> mayBeOrder = this.orderRepository.findById(id);
if (mayBeOrder.isPresent()) {
return HttpResponse.created(mayBeOrder.get());
}
return HttpResponse.notFound();
}

@Get
public HttpResponse<List<Order>> getAllOrders() {
return HttpResponse.ok(this.orderRepository.getAllOrders());
}

@Put
public HttpResponse<Order> addOrder(@Body @Valid Order order) {
Order savedOrder = orderRepository.save(order);
return HttpResponse.ok(savedOrder);
}

We also would now change the database from MySQL to Postgres. For this, we would add the Postgres driver dependency and the corresponding connection string in the properties file.

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.18</version>
</dependency>
----------------------------------------------------------------datasources:
default:
url: jdbc:postgresql://database.ashdjsirje.eu-central-1.rds.amazonaws.com/orders_db?characterEncoding=UTF-8
username: postgres
password: nopass
driverClassName: org.postgresql.Driver

I had to switch to Postgres because the MySQL driver was unstable when using it in a native image. It could not deserialize the object properly when it was being fetched.

With this, we are ready to build the application and deploy it on the Lambda JVM runtime

Deploying with JVM runtime.

Once we are ready, We can then trigger the lambda in the test section using the following payload.

{
"body": "{\"name\":\"Order from Lambda application\"}",
"resource": "/",
"path": "/",
"httpMethod": "PUT"
}

We can now also retrieve the persisted orders by making a GET call as follows.

{
"resource": "/",
"path": "/",
"httpMethod": "GET"
}

Once you hook the lambda function with an API getaway, the input request is similar to the one we just sent to the function as an input.

Now with this, let's look at the statistics I got for lambda configured with 512MB

Let’s now look at improving the application performance by creating a Native image.

Deploying Native Image on AWS Lambda with Custom Runtime

Before building, we have to set one extra property in the pom.xml file. we need to set the main class.

<exec.mainClass>io.micronaut.function.aws.runtime.MicronautLambdaRuntime</exec.mainClass>

Now we can then run the following command to build the image.

./mvnw clean package -Dpackaging=docker-native -Dmicronaut.runtime=lambda

It may take around 3–5 minutes to build the zip file containing the native image depending on your system.

Next, let's create an AWS lambda function with the custom runtime option “Provide your own bootstrap on Amazon Linux 2”, and then upload the zip file.

This time you don’t need to set any handler function as the bootstrap file in the zip will start the application.

You can now trigger the application with the same tests from above to persist an order or to get a list of orders.

With native images, we get a significant improvement with the init time being only around 932ms during cold start and the subsequent PUT requests came close to 16ms.

Here are the statistics I got.

With this, we are at the end of series of articles exploring Micronaut. But this is not the end. I would be exploring more frameworks and Micronaut will be on my radar for new features.

Stay tuned with new trends in frameworks and software development in general by subscribing to my newsletter on RefactorFirst.com to get updates on more such articles. Also, follow me on Twitter.

Software Craftsman, Tech Enthusiast. I run https://refactorfirst.com to post all my articles