Serverless Microservices with Spring Boot and Spring Data

A little over a year ago, I developed the original REST APIs for Tendril's MyHome mobile application using Amazon Web Service's API Gateway and Lambda. Serverless technologies were still considered bleeding edge at the time - scarce documentation, immature or non-existent build/deploy tools, etc. I even wrote custom build scripts to integrate versioning and deployment of Lambda functions to my development workflow. In addition, API Gateway's admin console was easy enough to navigate, but you still had to click yourself to death configuring the endpoints and Lambda function integration. Despite of all the pains and manual configuration, I felt the technology showed a lot of promise. I thought then - and I still do - that Serverless is the future. Docker container-based deployments might be the "standard" now, but I believe Serverless Architecture is the next evolution of PaaS (Platform as a Service).

SERVERLESS FRAMEWORK

A lot has happened since I first dabbled into the Serverless world over a year ago. The Serverless Framework has become the De Facto toolkit for building and deploying Serverless functions or applications. The Serverless Framework community has done a great job advancing the tools around Serverless architecture. All of the manual configuration required to set up API Gateway and AWS Lambda have been replaced by a simple YML configuration file like the one below:

serverless.yml

In the example above, notice that all you have to do is define the function handler and specify REST endpoints that will trigger the function.

In the Serverless community there is debate among developers on whether a single AWS Lambda function should only be responsible for a single API endpoint.. My answer, based on my real-world production experience, is NO.

Imagine if you are building a set of APIs with 10 endpoints and you need to deploy the APIs to DEV, STAGE and PROD environments. Now you are looking at 30 different functions to version, deploy and manage - not to mention the Copy & Paste code and configuration that will result from this type of set-up. NO THANKS!!!

I believe a more pragmatic approach is 1 Lambda Function == 1 Microservice.

For example, if you were building a User Microservice with basic CRUD functionality, you should implement CREATE, READ, UPDATE and DELETE in a single Lambda function. In the code, you should resolve the desired action by inspecting the request or the context.

If you are really concerned about the volume of requests and scaling the application, one strategy is to create 2 User Microservice Lambda functions - UserQuery function (responsible for reads) and UserCommand function (responsible for writes). The UserCommand function could point to the master database while the UserQuery function could point to the read replica database(s).

SPRING BOOT

Another controversial discussion around JVM Serverless implementations is the use of application containers like the Spring Framework. I can think of a few reasons why not to use Spring in a Serverless environment:

  • Bootstrapping Spring may add a few hundred milliseconds to the container start time during "cold starts"
  • Increased memory requirements
  • Increased size of the binaries due to Spring libraries

However, I can also think of A LOT OF REASONS why to use Spring. Here are just a few:

  • Dependency Injection and Inversion of Control
  • Abstraction utilities and APIs around databases, messaging frameworks, transactions, remote procedures, etc.
  • Integration templates for third party service providers and technologies

At the end of the day, I understand the trade-offs and I still see great value in integrating Spring to my Serverless projects.


Okay, enough with the fluff. Let's build a sample Serverless project using Spring Boot!!! Let's start with the main handler.

Lambda Handler

This is the entry point of the Lambda function. AWS will create an instance of this class and call the handleRequest method for every request it receives. It will hold on to the same LambdaHandler instance for the life of the container. This is where I instantiate the Spring ApplicationContext. Notice that the ApplicationContext is cached automatically via Groovy's @Memoized annotation. This means that the Spring Container is started when the function is first invoked, but is reused in subsequent invocations.

You might be wondering what the Request and Response objects look like. Here is the code:

Dispatcher Service

You may notice that the LambdaHandler above forwards all requests to the DispatcherService. The Spring container auto-injects all available Handler implementations to the DispatcherService and the dispatch method is responsible for routing the request to the appropriate handler. I chose this strategy because it allows me to add another handler/route just by creating a new class that implements the Handler interface. Spring's dependency injection mechanism automatically handles the registration of the handler instance.

Handler Interface

All request handlers must implement this interface. The DispatcherService above evaluates the result of the route method and if true, the respond method is called. You will see concrete examples below.

Get Users Handler

Here is the first example of a Handler implementation. Ignore the @Autowired UserRepository userRepository for now. It's simply a database abstraction class. The important things to note here are the route and respond methods. To summarize, this handler will only respond to the request if the value returned by route is true. In this case, this handler will respond to GET /users requests.

Notice how small, simple and testable the handler implementation is!

Find Users By Last Name Handler

Next is the handler for GET /users/{lastName}. It resolves the lastName value from the path parameter. Again, the implementation is very simple and straight forward.

Create User Handler

This last handler responds to POST /users/create?firstName={firstName}&lastName={lastName}. It inserts a new user to the database. In the real world, it's not a good idea to pass data to POST requests via query strings, but I want to show that resolving query strings from the request is quite trivial.

SPRING DATA

Here is where some really cool Spring Data magic happens. This project uses the Spring Data JPA module. With Spring Data JPA, if you set-up the Entity objects correctly - these are the Database ORM mappings - Spring can instrument the code and auto generate CRUD operations!!!

Here's an example of a User entity that maps to a user table in the database:

User Entity

The code below may seem like dark magic for someone who hasn't worked on the Spring Data project. However, just by creating a custom Interface that extends Spring's CrudRespositoryInterface, you gain access to database CRUD operations defined here without writing a single line of SQL code.

User Repository

Also, notice the findByLastName method above. Again, no need to implement the method or the query manually. Spring Data can inspect the column properties in the User entity and can auto-generate the SQL queries.

Examples:
The GetUsers handler calls userRepository.findAll()
The CreateUser handler calls userRepository.save(user)
The FindUsersByLastName handler calls userRepository.findByLastName(lastName)

However, if you look at the code you won't see concrete implementations of these interface methods. Spring Data handles it for you.

DEMO PROJECT

You can see all of the code in action by downloading and running the sample project: springboot-aws-lambda

Clone the sample project

To build, run: ./gradlew clean build

To deploy, run: serverless deploy

Once deployed, you should see a similar output from Serverless:

Serverless: Removing old service versions…
Serverless: Uploading CloudFormation file to S3…
Serverless: Uploading service .zip file to S3…
Serverless: Updating Stack…
Serverless: Checking Stack update progress…
..........
Serverless: Stack update finished…

Service Information
service: springboot-lambda
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users
  GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users/{lastName}
  POST - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users/create
functions:
  springboot-lambda-dev-users: arn:aws:lambda:us-east-1:xxxxx:function:springboot-lambda-dev-users

Test the new endpoints:

GET Users: curl https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users

GET Users By Last Name: curl https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users/Jackson

POST (Create) User: curl -X "POST" "https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users/create?firstName=Rowell&lastName=Belen"

PERFORMANCE

Now let's do a simple load test using the simple command-line tool seige.

siege -c100 -d1 -r20 https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/users

For this test, we launched 2000 requests with 100 concurrent users and a 1 second delay per request.

Transactions:             2000 hits
Availability:             100.00 %
Elapsed time:             19.74 seccs
Data transferred:         0.52 MB
Response time:            0.21 secs
Transaction rate:         101.32 trans/sec
Throughput:               0.03 MB/sec
Concurrency:              21.68
Successful transactions:  2000
Failed transactions:      0
Longest transaction:      1.46
Shortest transaction:     0.15

Here are the results for 4000 requests:

Transactions:             4000 hits
Availability:             100.00 %
Elapsed time:             38.87 secs
Data transferred:         1.04 MB
Response time:            0.24 secs
Transaction rate:         102.91 trans/sec
Throughput:               0.03 MB/sec
Concurrency:              24.71
Successful transactions:  4000
Failed transactions:      0
Longest transaction:      1.17
Shortest transaction:     0.14

As you can see, the Spring Lambda function handled the load quite well - no errors, no throttling, 100% availability, and the average response time was just around 0.21s - 0.24s. For real production tests however, consider an open source tool like Gatling or a cloud-based load testing software like LoadImpact.


VERT.X UPDATE!

I created a separate branch to use Vert.x's Event Bus instead of the DispatcherService(POJO) above.

You can find the Vert.x version of the code here: https://github.com/bytekast/springboot-aws-lambda/tree/vertx




Rowell Belen
Boulder, CO