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.
For the sake of this tutorial, the instance size or tier of MongoDB Atlas is not too important. In fact, an M0 instance, which is free, will work fine. You could also use a serverless instance 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 getting started guide.
Going into this tutorial, you might start with the following boilerplate AWS Lambda code for Java:
package example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
public class Handler implements RequestHandler<Map<String,String>, Void>{
@Override
public void handleRequest(Map<String,String> event, Context context) {
// Code will be in here...
return null;
}
}
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:
plugins {
id 'java'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
implementation 'com.amazonaws:aws-lambda-java-core:1.2.2'
implementation 'com.amazonaws:aws-lambda-java-events:3.11.1'
implementation 'org.slf4j:slf4j-log4j12:1.7.36'
runtimeOnly 'com.amazonaws:aws-lambda-java-log4j2:1.5.1'
}
test {
useJUnitPlatform()
}
task buildZip(type: Zip) {
into('lib') {
from(jar)
from(configurations.runtimeClasspath)
}
}
build.dependsOn buildZip
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:
dependencies {
// Previous boilerplate dependencies ...
implementation 'org.mongodb:bson:4.10.2'
implementation 'org.mongodb:mongodb-driver-sync:4.10.2'
}
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.
package example;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import org.bson.BsonDocument;
import org.bson.Document;
import org.bson.conversions.Bson;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Handler implements RequestHandler<Map<String,String>, Void>{
private final MongoClient mongoClient;
public Handler() {
mongoClient = MongoClients.create(System.getenv("MONGODB_ATLAS_URI"));
}
@Override
public void handleRequest(Map<String,String> event, Context context) {
MongoDatabase database = mongoClient.getDatabase("sample_mflix");
MongoCollection<Document> collection = database.getCollection("movies");
// More logic here ...
return null;
}
}
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 Handler
constructor method:
public Handler() {
mongoClient = MongoClients.create(System.getenv("MONGODB_ATLAS_URI"));
}
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:
@Override
public void handleRequest(Map<String,String> event, Context context) {
MongoDatabase database = mongoClient.getDatabase("sample_mflix");
MongoCollection<Document> collection = database.getCollection("movies");
// More logic here ...
return null;
}
Because this example was meant to only be enough to get you going, we’re using the sample datasets that are available for MongoDB Atlas users. It doesn’t really matter what you use for this example as long as you’ve got a collection with some data.
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:
public class Handler implements RequestHandler<Map<String,String>, List<Document>>{
private final MongoClient mongoClient;
public Handler() {
mongoClient = MongoClients.create(System.getenv("MONGODB_ATLAS_URI"));
}
@Override
public List<Document> handleRequest(Map<String,String> event, Context context) {
MongoDatabase database = mongoClient.getDatabase("sample_mflix");
MongoCollection<Document> collection = database.getCollection("movies");
// More logic here ...
return null;
}
}
Notice that the implemented RequestHandler
now uses List<Document>
instead of Void
. The return type of the handleRequest
function has also been changed from void
to 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 Document
instead.
If we want to query MongoDB and return the results, we could do something like this:
@Override
public List<Document> handleRequest(Map<String,String> event, Context context) {
MongoDatabase database = mongoClient.getDatabase("sample_mflix");
MongoCollection<Document> collection = database.getCollection("movies");
Bson filter = new BsonDocument();
if(event.containsKey("title") && !event.get("title").isEmpty()) {
filter = Filters.eq("title", event.get("title"));
}
List<Document> results = new ArrayList<>();
collection.find(filter).limit(5).into(results);
return results;
}
In the above example, we are checking to see if the user input data event
contains 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 find
operation 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:
./gradlew build
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:
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.
We used MONGODB_ATLAS_URI
in the code, and the actual value would look something like this:
mongodb+srv://<username>:<password>@examples.170lwj0.mongodb.net/?retryWrites=true&w=majority
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:
{
"title": "Batman"
}
You should see different results reflecting what was added in the code.
You just saw how to create a serverless function with AWS Lambda that interacts with MongoDB. In this particular example, Java was the star of the show, but similar logic and steps can be applied for any of the other supported AWS Lambda runtimes or MongoDB drivers.
If you have questions or want to see how others are using MongoDB Atlas with AWS Lambda, check out the MongoDB Community Forums.
This content first appeared on MongoDB.