Serverless Development with AWS Lambda and MongoDB Atlas Using Java
Rate this tutorial
So you need to build an application that will scale with demand and a database to scale with it? It might make sense to explore serverless functions, like those offered by AWS Lambda, and a cloud database like MongoDB Atlas.
Serverless functions are great because you can implement very specific logic in the form of a function and the infrastructure will scale automatically to meet the demand of your users. This will spare you from having to spend potentially large amounts of money on always on, but not always needed, infrastructure. Pair this with an elastically scalable database like MongoDB Atlas, and you've got an amazing thing in the works.
In this tutorial, we're going to explore how to create a serverless function with AWS Lambda and MongoDB, but we're going to focus on using Java, one of the available AWS Lambda runtimes.
To be successful with this tutorial, there are a few requirements that must be met prior to continuing.
- Must have an Amazon Web Services (AWS) account.
- Must have Gradle or Maven, but Gradle will be the focus for dependency management.
For the sake of this tutorial, the instance size or tier of MongoDB Atlas is not too important. In fact, an M0 instance, , will work fine. You could also use a which pairs nicely with the serverless architecture of AWS Lambda. Since the Atlas configuration is out of the scope of this tutorial, you'll need to have your user rules and network access rules in place already. If you need help configuring MongoDB Atlas, consider checking out the .
Going into this tutorial, you might start with the following boilerplate AWS Lambda code for Java:
You can use a popular development IDE like IntelliJ, but it doesn't matter, as long as you have access to Gradle or Maven for building your project.
Speaking of Gradle, the following can be used as boilerplate for our tasks and dependencies:
Take note that we do have our AWS Lambda dependencies included as well as a task for bundling everything into a ZIP archive when we build.
With the baseline AWS Lambda function in place, we can focus on the MongoDB development side of things.
To get started, we're going to need the MongoDB driver for Java available to us. This dependency can be added to our project's build.gradle file:
The above two lines indicate that we want to use the driver for interacting with MongoDB and we also want to be able to interact with BSON.
With the driver and related components available to us, let's revisit the Java code we saw earlier. In this particular example, the Java code will be found in a src/main/java/example/Handler.java file.
In the above code, we've imported a few classes, but we've also made some changes pertaining to how we plan to interact with MongoDB.
The first thing you'll notice is our use of the
We're establishing our client, not our connection, outside of the handler function itself. We're doing this so our connections can be reused and not established on every invocation, which would potentially overload us with too many concurrent connections. We're also referencing an environment variable for our MongoDB Atlas URI string. This will be set later within the AWS Lambda portal.
It's bad practice to hard-code your URI string into your application. Use a configuration file or environment variable whenever possible.
Next up, we have the function logic where we grab a reference to our database and collection:
We're on our way to being successful with MongoDB and AWS Lambda!
With the client configuration in place, we can focus on interacting with MongoDB. Before we do that, a few things need to change to the design of our function:
Notice that the implemented
Void. The return type of the
handleRequestfunction has also been changed from
List<Document>to support us returning an array of documents back to the requesting client.
While you could do a POJO approach in your function, we're going to use
If we want to query MongoDB and return the results, we could do something like this:
In the above example, we are checking to see if the user input data
eventcontains a property "title" and if it does, use it as part of our filter. Otherwise, we're just going to return everything in the specified collection.
Speaking of returning everything, the sample data set is rather large, so we're actually going to limit the results to five documents or less. Also, instead of using a cursor, we're going to dump all the results from the
findoperation into a
List<Document>which we're going to return back to the requesting client.
We didn't do much in terms of data validation, and our query was rather simple, but it is a starting point for bigger and better things.
The project for this example is complete, so it is time to get it bundled and ready to go for deployment within the AWS cloud.
Since we're using Gradle for this project and we have a task defined for bundling, execute the build script doing something like the following:
If everything built properly, you should have a build/distributions/*.zip file. The name of that file will depend on all the naming you've used throughout your project.
With that file in hand, go to the AWS dashboard for Lambda and create a new function.
There are three things you're going to want to do for a successful deployment:
- Add the environment variable for the MongoDB Atlas URI.
- Upload the ZIP archive.
- Rename the "Handler" information to reflect your actual project.
Within the AWS Lambda dashboard for your new function, click the "Configuration" tab followed by the "Environment Variables" navigation item. Add your environment variable information and make sure the key name matches the name you used in your code.
MONGODB_ATLAS_URIin the code, and the actual value would look something like this:
Just remember to use your actual username, password, and instance URL.
Next, you can upload your ZIP archive from the "Code" tab of the dashboard.
When the upload completes, on the "Code" tab, look for "Runtime Settings" section and choose to edit it. In our example, the package name was example, the Java file was named Handler, and the function with the logic was named handleRequest. With this in mind, our "Handler" should be example.Handler::handleRequest. If you're using something else for your naming, make sure it reflects appropriately, otherwise Lambda won't know what to do when invoked.
Take the function for a spin!
Using the "Test" tab, try invoking the function with no user input and then invoke it using the following:
You should see different results reflecting what was added in the code.