JavaEE application migration to Serverless architecture with the lowest code impacts

JavaEE application migration to Serverless architecture with the lowest code impacts


5 years ago, we migrated our JavaEE application to AWS Cloud with Lift and Shift methodology by using EC2. Today all our new applications are building a serverless way directly on AWS, using AWS Lambda. In order to homogenize our way to operate our applications, we are willing to migrate as many existing applications as possible to the same serverless architecture.

However, in order to limit impacts in terms of cost and regression risks, we wish to limit as much as possible any changes done on the application part of the code. Our existing application being written with EJB, they already respect microservices philosophy which made it possible to keep the structure of the project.

Existing technical stack before migration:

Stack Technique avant migration

Our applications are executed on the application server Apache TomEE. The structuration into micro services modules is done with EJB (Enterprise Java Bean). We use JNDI to lookup and access an EJB instance, either from an EJB within another application server or from a local client.

We also use JMS (Java Message Service) with an ActiveMQ implementation.

Here are the main changes:

1) EJB to Lambda:
In order to keep the same project structure, we chose the following design: 1 EJB = 1 Lambda. For each service interface, we write a lambda function handler and we overwrite inter-EJB calls by lambda calls thanks to a proxy that maps the existing JNDI names to the new lambda names. As each EJB may expose several public methods, we configured the lambda handler to accept an input parameter that is used to know which public method to call.

2) API exposure:
Previously we used an AWS Application Load Balancer to expose our services that were deployed on EC2 in an autoscaling group. We have replaced this Load Balancer with AWS API Gateway in order to easily map the routes to the appropriate lambdas.

3) Message queues management:
Our application manages asynchronous messages that are published on a JMS queue (an example of use case is data transfers between multiple systems). AWS SQS now replaces our ActiveMQ JMS implementation.

4) Call scheduling:
For regular tasks with a defined frequency, we used Job Quartz with a specified cron expression and mapped to the JAVA method to call. We use Cloudwatch Events to define one rule per process to execute. The AWS Cloudwatch event target can either be a lambda or an AWS Batch for longer processes.

the architecture of our simplified team management project

Here you have the architecture of our simplified team management project, historically developed with JavaEE hosted on EC2, migrated to a serverless application with AWS Lambda. We can see that each EJB (History, User, Project, Employee) has been transformed to a lambda written in JAVA.

Our challenges:

  • One EJB public method could have up to 7 inter-EJB calls in order to execute the process end-to-end. By naively applying our 1 EJB = 1 Lambda rule, 1 lambda call could launch up to 6 other lambda sequentially.

    In order to improve performances, we changed from 7 to maximum 4 inter-lambdas calls. This was achieved by injecting some utilities EJB as libraries in the lambda functions. It increased the code coupling but reduced the network latency and the number of cold starts. The code changes are not done inside the services but are mutualized in a handler class that manages the inter-services calls, depending on context and on the service to call : either an EJB call (historical configuration), a lambda invocation or those service instantiation.

  • By deploying our application with Java Lambdas, we encountered some Cold Start performance issues. Those performance issues were mainly due to the database connection. First we migrated our hibernate utility class to its own lambda. Then we optimized this lambda by using Quarkus and GraalVM in order to optimize the lambda runtime. Thanks to these changes, we achieved acceptable performances.

Next Steps:

  • Ability to upgrade only specific lambda, with a map of version dependencies between the micro services
  • Continue to improve our performances (for example by applying GraalVM to all our code and not only to the database service)
  • Migrate into serverless more existing application
  • Continue to evangelize serverless best practices inside our organization

Interested in Serverless migration topic? Come and join us for an internship at Edifixio!