Effectively running Java on ServerlessSpring4 min

Spring Boot is the most popular Java application framework with +60% of Java developers reportedly using it. There is a broad ecosystem (libraries, tools and resources) and developer experience. Spring Boot on a server initializes the environment once and handles multiple requests with the same instance, whereas in AWS Lambda an execution environment is initialized on first invocation and can handle only one request at a time. Another environment is started if another request comes in while the execution environment is busy.

Spring Cloud for AWS

Spring Cloud AWS supports a subset of AWS services which drastically reduces the time needed to use their capabilities in a Spring Boot application. AWS services like Amazon RDS, Amazon SQS, Amazon SNS, Amazon S3, and more are already available. Spring Cloud AWS using Spring APIs and idioms to make it easy for Spring Boot developers to adapt above AWS services. For example, Spring Messaging API with SQS, Spring Cache API with Amazon ElasticSearch, emails via Amazon SES, bootstrap property sources with AWS Parameter Store or Secrets Manager.

Spring Cloud Function

Spring Cloud Function promotes the implementation of business logic via Function interface. You can use this in conjunction with AWS Lambda for payload translation, and to leverage Spring Boot features in functional environment.

Spring journey to AWS Lambda

Spring journey to AWS Lambda contains four steps:

Migrate

AWS Lambda is invoked via Events (API Event, S3 Object Event, SQS Event, etc.). Events follow a certain structure, and this is different from accepting an HTTP connection. AWS Serverless Java Container is a library that acts as a conduit between Events and HTTP. It transforms events so that Spring can handle them as if it was an HTTP request.

Using AWS Serverless Java Container

If you still need to use @RestController in your SpringBoot application running on AWS Lambda, then AWS Serverless Java Container can be used which transforms events to framework specific HTTP Request structure. This is mainly used when routing via @RestController, rely on POJO serialization, and return HTTP status codes.

All you have to do is to add the library to your code and provide a configuration class.

AWS Serverless Java Container now supports Java 17 and SpringBoot 3. Refer this pet-store sample to learn more about its use.

Optimize

  • Right size the application memory setting with AWS Lambda power tuning.
  • Enable AWS Lambda SnapStart for fast function startups
  • Use Tiered Compilation to reduce cold-starts (for Java 11)
  • Use AWS SDK & Initialization best practices
  • Pre-warm execution environments with Provisioned Concurrency.

Refactor

  • Improve performance and memory footprint by refactoring the application
  • Using light-weight dependencies, for example:
    • spring-data-jpa vs spring-data-jdbc
    • jackson vs jackon-jr
    • log4J2 vs SLF4J SimpleLogger
  • Move to a functional approach
    • Avoid the overhead of providing an own HTTP server (API Gateway takes care of this)
    • Use a functional approach using Spring Cloud Functions and directly handle the event from API Gateway
    • RestController vs FunctionHandler<Event>

Prefer using Function interface, as mentioned above with Spring Cloud Function. An AWS Lambda handler would look as the following:

@Component
public class UnicornPostFunction implements Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

  @Override
  public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent input) {
    try {
      var unicorn = objectMapper.readValue(input.getBody(), Unicorn.class);
      var savedUnicorn = unicornService.createUnicorn(unicorn);
      return new APIGatewayProxyResponseEvent().withStatusCode(200).withBody(objectMapper.writeValueAsString(savedUnicorn));
    } catch (Exception e) {
      logger.error("Error creating unicorn", e);
      return new APIGatewayProxyResponseEvent().withStatusCode(500).withBody("Error creating unicorn");
    }
  }
}

instead of @RestController:

@RestController
public class UnicornController {
  //...
  @PostMapping("/unicorns")
  public ResponseEntity<Unicorn> createUnicorn(@RequestBody Unicorn unicorn) {
    try {
      var savedUnicorn = unicornService.createUnicorn(unicorn);
      return ResponseEntity.ok(savedUnicorn);
    } catch (Exception e) {
      logger.error("Error creating unicorn", e);
      throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Error creating unicorn", e);
    }
  }
}

Accelerate

GraalVM Native image

  • Spring Boot 3 provides native support for GraalVM native images
  • Create a native binary - usage with AWS Lambda custom runtime
  • Sub-second application startup

Use alternative compile time frameworks on the JVM (Micronaut, Quarkus)

  • Cloud-Native frameworks – Built for the cloud
  • Allows you to shift certain tasks to build time (e.g., Dependency Injection)
  • It can further improve application start-up and memory footprint

Considerations

  • Function granularity and least privilege
  • Connection pooling (Refer "Using RDS Proxy" page to learn more about connection pooling in AWS Lambda)
  • Endpoint-based functionality (Spring Actuator, Metrics scraping)
  • File-System logging
  • Caching

Created by:

Lavanya Tangutur
Lavanya TanguturSr. Technical Account Manager
Maximilian Schellhorn
Maximilian SchellhornMaximilian Schellhorn works as a Solutions Architect at Amazon Web Services. His recent work focuses on migrating, optimizing and running Serverless Java applications on AWS Lambda.