Skip to content

Micronaut and AWS Lambda

Posted on:November 24, 2020 at 09:22 PM

A year ago, I was developing an enterprise solution using aws lambda with Java and at that time there was no support for high level tools and frameworks in the ecosystem with respect to Serverless space something like Spring Boot. I’ve used spring cloud in aws lambda but it didn’t work for the solution we are building. So I’ve started developing with the plain AWS SDK library and Speedment ORM but after building the few services I've felt a couple of bottlenecks.

In the late last year and early this year, two frameworks Micronaut and Quarkus came to the light which supports Serverless space in the Java ecosystem. Micronaut is inspired from spring but there is no runtime penalty for holding metadata for configuration and dependency injection. Every information is handled at compile time using AST processors. 

Table of contents

Open Table of contents

Code Examples and Numbers

This article is accompanied by a working code example on GitHub.

TypeMemory(In MB)Startup TimePackage Size(In MB)
Micronaut Lambda23047 s31
Micronaut Lambda Native Image2304907 ms26

To show case the productivity of the micronaut we are going to build a minimal blog post api using Micronaut, AWS Lambda and Postgres RDS.

Choose How Lambda is Triggered

To generate the micronaut module naviage to https://micronaut.io/launch/ and select application type as Application with the following features

Now generate the project and import in IDE.

The Micronaut application type you select depends on the triggers you want to support. To respond to incoming HTTP requests (e.g. AWS Lambda Proxy integrations in API Gateway), you can choose either Application or Serverless Function. For other triggers, such as consuming events from a queue or running on a schedule, choose Serverless Function.

Application TypeTrigger Type
Application or Serverless FunctionHTTP requests to a single endpoint
ApplicationHTTP requests to multiple endpoints
Serverless FunctionS3 events, events for a queue, schedule triggers etc.

Lambda Handlers

Lambda function's handler is the method in your function code that processes events. When your function is invoked, Lambda runs the handler method. When the handler exits or returns a response, it becomes available to handle another event. The aws-lambda-java-core library defines two interfaces for handler methods. When coding your functions with Micronaut, you don't implement those interfaces directly. Instead, you extend or use its Micronaut equivalents.

Application TypeAWS Handler InterfaceMicronaut Handler Class
Serverless FunctionRequestHandlerMicronautRequestHandler
Serverless FunctionRequestStreamHandlerMicronautRequestStreamHandler
ApplicationRequestStreamHandler <AwsProxyRequest , AwsProxyResponse>MicronautLambdaHandler

You can read more about aws lambda guide in the micronaut page.

Database Tables and Entities

Consider the following tables where posts and tags exhibit a many-to-many relationship between each other. The many-to-many relationship is implemented using a third table called post_tags which contains the details of posts and their associated tags.

Post Entity

@Entity
@Table(name = "post")
public class Post implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String title;

    private String description;

    private String content;

    @Column(name = "posted_at")
    private Date postedAt = new Date();

    @Column(name = "last_updated_at")
    private Date lastUpdatedAt = new Date();

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "post_tags", joinColumns = {@JoinColumn(name = "post_id")},
            inverseJoinColumns = {@JoinColumn(name = "tag_id")})
    private Set<Tag> tags = new HashSet<>();

    public Post() {
    }

    public Post(String title, String description, String content) {
        this.title = title;
        this.description = description;
        this.content = content;
    }
}

Tag Entity

@Entity
@Table(name = "tags")
public class Tag implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "tags", fetch = FetchType.EAGER)
    private Set<Post> posts = new HashSet<>();

    public Tag() {
    }

    public Tag(String name) {
        this.name = name;
    }
}

Defining the Repositories

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {

    @Query(nativeQuery = true, value = "select * from post where id in(select distinct(post_tags.post_id) " +
            "from post_tags where post_tags.tag_id in(:id))")
    List<Post> filterByTag(List<Long> id);
}
@Repository
public interface TagRepository extends JpaRepository<Tag, Long> {

    @Query("Select t.id from Tag t where t.name in(:names)")
    List<Long> findByName(List<String> names);
}

Controller

@Controller("/v1")
public class PostTagController {

    private final PostTagService postTagService;

    @Inject
    public PostTagController(PostTagService postTagService) {
        this.postTagService = postTagService;
    }

    @Post(value = "/new-post", produces = MediaType.APPLICATION_JSON)
    public Response<CreatePostResponse> createPost(@Body CreatePostRequest createPostRequest) {
        log.info("In createPost");

        var response = new Response<CreatePostResponse>();

        try {
            var apiResponse = this.postTagService.createPost(createPostRequest);
            updateResponse(response, apiResponse, "200", "");
        } catch (BlogException e) {
            log.error("Error while creating post - " + e);
            updateResponse(response, null, "500", e.getMessage());
        }

        log.info("Return from createPost");
        return response;
    }

    @Get(value = "/list-tags", produces = MediaType.APPLICATION_JSON)
    public Response<List<String>> listTags() {
        log.info("In listTags");

        var response = new Response<List<String>>();
        try {
            var apiResponse = this.postTagService.listTags();
            updateResponse(response, apiResponse, "200", "");
        } catch (BlogException e) {
            log.error("Error while fetching post - " + e);
            updateResponse(response, null, "500", e.getMessage());
        }

        log.info("Return from listTags");
        return response;
    }
}

This feels very similar to spring boot and it's more productive than before. Once you have the service ready you can deploy with few steps.

Build the package with mvn clean package and deploy the Jar from target folder.

Define the handler with com.blog.handler.BlogHandler which is a custom class that extends MicronautLambdaHandler. If you choose not to define a custom handler, then we can set default MicronautLambdaHandler using io.micronaut.function.aws.proxy.MicronautLambdaHandler in the aws lambda.

As you can see in the above image, it takes around nearly 7 seconds to initialize the lambda which means during cold starts it will take a minimum of 7 seconds to load our application. There are plenty of approaches to improve the cold start but here we will use native images to improve our cold start time.

Native Image

It's an AOT compiler for Java code to a standalone executable, called a native image. This executable includes the application classes, classes from its dependencies, runtime library classes, and statically linked native code from JDK. It does not run on the Java VM, but includes necessary components like memory management, thread scheduling, and so on from a different runtime system, called “Substrate VM”. Substrate VM is the name for the runtime components (like the deoptimizer, garbage collector, thread scheduling etc.). The resulting program has faster startup time and lower runtime memory overhead compared to a JVM.

Installation

In order to generate native image module, we need to choose two more features when generating micronaut module via https://micronaut.io/launch/

Once you create the native module, You can define the same entities, repositories and controller components as mentioned above. To create native image

When you deploy native image in AWS Lambda, we need to choose custom runtime as shown below and upload the ZIP file.

With native image we can see the initialization is less than one second(907ms). We have reduced our cold start time from 7 seconds to nearly 1 second.

Caveats:

Conclusion