Serverless on AWS Lambda With Kotlin + Micronaut + Graal VM

A quick look at Caribou — the SaaS developer tool that helps with technical debt metrics

Sakis Kaliakoudas
Better Programming

--

Photo by Marc Reichelt on Unsplash

In this article, we’ll review the technology stack decisions for Caribou, a developer tool that helps developers use data-driven insights to manage their technical debt. We will explore some of the pros and cons of serverless architectures and how easy it is to use serverless with a JVM language.

About a year ago we started discussing about the main components and the software stack that we would use to build Caribou.

Caribou is a SaaS developer tool that can be installed on your GitHub account as a GitHub application and provide insights about how you are progressing on removing technical debt from your codebase.

For example, if you want to move your project from Java to Kotlin and you want to track this migration then all you need to do is define a rule to tell Caribou which files you are interested in tracking.

Caribou then runs an analysis after every pull request is merged, providing you with real-time data about how the migration is progressing, when the estimated finish date is, who is contributing to it, and more!

Caribou in action!

The main technical components needed for Caribou are:

  • A Web application to enable users to define their migrations, and also see all migration metrics around the code changes that they are introducing.
  • A backend with a REST API to serve the web application along with a database to store the meta-data around the migrations that the users are defining and how those are progressing.
  • A mechanism for users to authenticate and provide Caribou access to their GitHub repositories so that it can monitor incoming pull requests and analyze them as and when required.

When it came to designing our solution and making technical decisions, the most important criteria was development speed; we wanted to get an MVP out quickly, and then iterate from there based on users’ feedback. This meant choosing technologies and tools that we were familiar with, prioritizing the usage of off-the-shelf products rather than building things ourselves, and choosing tools that had the smallest learning curve or had good documentation and communities around them that would help us get up to speed fast.

This article will focus on the 3 main decision choices around the backend, namely:

  • What language it would be built with.
  • What framework we would use to do the heavy lifting for us.
  • Whether we would make use of the new kid in the block, ‘Serverless’, or whether we would go with the established monolith or microservices approach.

What we ended up with was a backend written in Kotlin, built with a serverless architecture on AWS Lambda, using the Micronaut framework. Let’s now go through these 3 choices in more detail:

Why AWS Lambda

The first question that we set for ourselves is whether we would use a serverless architecture for our backend, or stick to a more traditional solution of one or more services deployed within containers.

Thinking about our original requirement around speed, serverless intrigued us, as it meant that we would need to take care of fewer things. For someone coming into backend development for the first time, the myriads of AWS container services (ECS, EKS, Fargate, Kubernetes to name a few) have a rather steep learning curve.

On the flip side, serverless has a very simple model that allows you to only worry about the code that you are executing, and let the platform service provider worry about everything else, as far as infrastructure and scalability are concerned.

To test our hypothesis that serverless would be faster and simpler for us to use than using containers, we built a simple dummy backend with AWS Lambda connected to a single REST API (using AWS API Gateway) and managed to get a sense of which one is the fastest to build for us. AWS lambda won hands down.

Infrastructure vs Container vs Function as a service, from https://tinyurl.com/caas-faas

Other than that it’s worth noting that:

  • Building with a serverless architecture would also mean that we would by definition end up with a modular system, where each lambda function only does one thing, has a very clear interface, and can be deployed independently.
  • Many platform providers provided a generous free tier (for example AWS is offering 1,000,000 function invocations per month for free), which would mean that we wouldn’t need to pay anything for our lambda executions for a while 🙂.

However, there were some downsides as well:

  • Serverless services haven’t been around for as much time as container services, and our intention of using it with Kotlin meant that there wouldn’t be as much content online on the topic to guide us.
  • If we were to use it with any JVM language like Kotlin, we would need to find a solution for the cold startup problem: it takes longer to execute JVM based languages as on the first lambda invocation you need to wait for the runtime environment to initialize, while consequent invocations do not face the same issue. This might sound like a small problem, but having a REST API return in 10–15 seconds can really kill the user experience, even if it doesn’t happen that often.
  • Another problem that we thought we might encounter with serverless was the fact that one of our functions would need to perform a git clone operation (in order to perform the technical debt analysis that Caribou provides) and that might time out in serverless architectures, given that all providers have specific timeout limits for their function invocations (for example AWS Lambda currently has a 900 seconds timeout).

After investigating these potential issues mentioned above, we decided to proceed with serverless as:

  • We found GraalVM native images to help with cold startup times as we’ll see later in this article.
  • After testing git clone operations even for bigger git repositories we thought that it is quite unlikely that they will take more than 15 minutes, so we treated that as an edge case. If we ever encountered this problem then a sparse checkout might be the answer for it.
Some pretty large git repositories that we tried cloning with Caribou. Based on that we were quite comfortable with the 900 seconds timeout limit on AWS lambda.

We specifically decided to go with AWS Lambda instead of Azure or GCP, mostly due to some familiarity with AWS from the past, but also due to a close friend having substantial experience with the AWS platform, meaning that we could potentially get some help if anything got horribly wrong!

P.S. If you want an awesome in-depth review of serverless architectures, make sure you check out this article.

Why Kotlin

Kotlin is a modern language built for the JVM coming from JetBrains. It came out about 10 years ago but started really becoming more popular with its introduction in Android development, back in 2018.

Coming from an Android development background myself and seeing how great this language is to use, how it’s helped us write more concise code with fewer bugs, and how complete the tooling/IDE around it is (JetBrains does a fabulous job with that 🙂), I naturally gravitated towards using it for Caribou as well. Using Kotlin also meant that we would be able to use Gradle as our build system, and leverage the experience of using Gradle for many years for mobile development.

After investigating for a while, the main challenge appeared to be the JVM startup times discussed above, if we were going to go down the serverless route. A quick test with Kotlin (the initial test was with Spring Cloud Function) showed that an AWS Lambda function would indeed take about 15 seconds to execute. Thankfully, 3 things happened:

  • We discovered some other more lightweight frameworks such as Micronaut and Quarkus that focused on faster startup times and having a smaller memory footprint. More on that in a moment.
  • We discovered the wonders of GraalVM native images.
  • There was some indication that AWS itself had acknowledged the problem and had come up with a solution, provisioned concurrency, which basically keeps some lambda functions always ready to fire up, removing the cold start problem altogether.

These findings led us to decide to go for Kotlin in the end, and we haven’t looked back since. We ended up not using provisioned concurrency as keeping lambda functions always ready to fire essentially emulates a backend running all the time, which kind of defeats the purpose of serverless (not to mention that it is substantially more expensive).

Why Micronaut

As discussed above, the first attempt was to use Spring, but this proved to be difficult as:

  • Startup times for functions were long. Spring is a pretty big framework that depends on reflection to startup, slowing it down.
  • There wasn’t great support in the Spring framework for AWS lambda at the time (I believe that since then things have improved).
  • There was no support for GraalVM native images at the time (it seems like there’s some experimental support these days)

So given Spring was not going to cut it, we started looking at the other frameworks mentioned above, that were focusing on startup time speed. We initially decided to assess both Quarkus and Microanut, starting with Micronaut, but we were so happy with Micronaut that we decided to just proceed with it! Overall, it seemed like it had a bigger focus on the serverless scenarios, as well as concrete AWS tutorials. Indeed, we very quickly had a lambda function with Micronaut running on AWS and it was faster than Spring (however it was still not at acceptable levels before using GraalVM however). Apart from that, Micronaut:

  • Has a great support community on Gitter
  • Has a ton of docs with examples of how the framework can be used.
  • Allows spinning up the whole Micronaut framework for unit tests, making it extremely easy to run end-to-end tests, without having to mock anything at all. This allows us to have hundreds of these integration tests, without a big impact on test execution time. I have to say, nothing beats running tests on the real framework, without any mocks.
  • Has a truly awesome IntelliJ IDEA plugin (only for the IntelliJ IDEA Ultimate edition) that makes it very easy to define database queries just by defining interface function names following a specific naming convention), and then having Micronaut implement these interfaces for you with the code that performs the SQL operations.
  • Has built-in support for Swagger, which made it really easy to provide the necessary details about our API to the frontend team.

Micronaut achieves it’s performance by focusing on not using reflection at all and using annotation processing instead to generate all the code required so that the framework can operate.

GraalVM

I think that GraalVM is worth its own section as part of this article as it has come out of nowhere and has won the hearts of many developers, offering an alternative to the JVM, that has better performance characteristics and makes the running of JVM languages possible for serverless. It has many usages, from using libraries written in other languages in your project all the way to combining JavaScript, Java, Ruby, and R on the same file!

The experience of integrating it with Micronaut and AWS lambda has definitely given us a few sleepless nights, as in some cases custom configuration is required for libraries that use reflection or dynamic proxies, but once you wrap your head around how GraalVM native image works, it then becomes a pretty streamlined process that rarely needs attention.

So was it worth using GraalVM? Absolutely! A REST API call that performed a cold start would take around 10 seconds to return just with Micronaut, while when using Graal VM native images that went down to about 2 seconds! (out of which the cold start is around 800ms)

The End Result

Here are the main components of the system that we ended up building:

Final Thoughts

Looking back, we are quite happy with the technology choices we made: AWS Lambda with Kotlin and Micronaut has helped this project move forward quite quickly, not to mention that it was really good fun building these systems out!

It was only the combination of these technologies plus GraalVM that led to this outcome, so we were lucky that when we started building Caribou these technologies were mature enough.

You can help your teams fight the war against technical debt by checking out Caribou here.

--

--