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.
- We need to encode and decode every request and response in each service
- Everytime when we add a new table or edit/modify any of the schemas we need to regenerate our entity classes via the speedment UI tool.
- We need to write many if and else statements to route the specific API endpoints to the corresponding service.
- Sometimes speedment open source ORM generated SQL queries are not optimized thus takes longer time in querying data.
- Lambda Cold Start Issue - A cold start happens when you execute an inactive Lambda function. The execution of an inactive Lambda function happens when there are no available containers, and the function needs to start up a new one and this takes 4-5 seconds. You can learn more about lambda cold start in this post.
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.
Type | Memory(In MB) | Startup Time | Package Size(In MB) |
Micronaut Lambda | 2304 | 7 s | 31 |
Micronaut Lambda Native Image | 2304 | 907 ms | 26 |
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
- postgres
- data-jpa
- aws-lambda
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 Type | Trigger Type |
Application or Serverless Function | HTTP requests to a single endpoint |
Application | HTTP requests to multiple endpoints |
Serverless Function | S3 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 Type | AWS Handler Interface | Micronaut Handler Class |
Serverless Function | RequestHandler | MicronautRequestHandler |
Serverless Function | RequestStreamHandler | MicronautRequestStreamHandler |
Application | RequestStreamHandler <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
- Post Repository
@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);
}
- Tag Repository
@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
- Install graalvm(Latest Version 20.3) using SDKMAN using
sdk install java 20.3.0.r11-grl
. - Set the default java version as graalvm 20.3 using
sdk default java 20.3.0.r11-grl
. - Finally, install native-image using command
gu install native-image
.
In order to generate native image module, we need to choose two more features when generating micronaut module via https://micronaut.io/launch/
- aws-lambda-custom-runtime
- graalvm
Once you create the native module, You can define the same entities, repositories and controller components as mentioned above. To create native image
- Package the module using the
mvn clean package
. This will generate the target jar. - Use native-image tool to build native image using
native-image -cp target/graal-wordpress--*.jar
. - Finally package the native image to deploy in aws lambda
chmod 777 bootstrap
chmod 777 graal-wordpress
zip -j function.zip bootstrap pg-graal-wordpress
- Define io.micronaut.function.aws.proxy.MicronautLambdaHandler in the aws lambda as handler.
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:
- Micronaut graal native image for MySQL is not stable at this time(This is fixed).
- Not all java libraries are directly supported with native images.
- The build time of the native image will take approximately 5-6 minutes depends on your machine.
Conclusion
- Micronaut and Graal VM are game changer for Java especially in the Serverless space.
- Micronaut helps you to become more productive when building API’s using AWS Lambda or any other serverless technologies.