Navigation

Build a Resilient Application with MongoDB Atlas

To write application code that takes full advantage of the always-on capabilities of MongoDB Atlas, you should:

  • Install the latest drivers.
  • Use the connection string provided by Atlas.
  • Use retryable writes and retryable reads.
  • Use a majority write concern and a read concern that makes sense for your application.
  • Handle errors in your application.

Install the Latest Drivers

First, install the latest drivers for your language from MongoDB Drivers. Drivers connect and relay queries from your application to your database. Using the latest drivers enables the latest MongoDB features.

Then, in your application, import the dependency:

    If you are using Maven, add the following to your pom.xml dependencies list:

    <dependencies>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-sync</artifactId>
            <version>4.0.1</version>
        </dependency>
    </dependencies>
    

    If you are using Gradle, add the following to your build.gradle dependencies list:

    dependencies {
      compile 'org.mongodb:mongodb-driver-sync:4.0.1'
    }
    
    // Latest 'mongodb' version installed with npm
    const MongoClient = require('mongodb').MongoClient;
    

    Connection Strings

    Note

    Atlas provides a pre-configured connection string. For steps to copy the pre-configured string, see Atlas-Provided Connection Strings.

    Use a connection string that specifies all the nodes in your Atlas cluster to connect your application to your database. If your cluster performs a replica set election and a new primary is elected, a connection string that specifies all the nodes in your cluster discovers the new primary without application logic.

    You can specify all the nodes in your cluster using either:

    The connection string can also specify options, notably retryWrites and writeConcern.

    Atlas-Provided Connection Strings

    If you copy your connection string from your Atlas cluster interface, the connection string is pre-configured for your cluster, uses the DNS Seedlist format, and includes the recommended retryWrites and w (write concern) options.

    To copy your connection string URI from Atlas:

    1. In MongoDB Atlas, under your project, select Clusters from the navigation panel.

    2. Click Connect on the cluster you wish to connect your application to.

    3. Select Connect Your Application as your connection method.

    4. Select your Driver and Version.

    5. Copy the connection string or full driver example into your application code. You must provide database user credentials.

      Note

      This guide uses SCRAM authentication through a connection string. If you want to learn about using X.509 certificates to authenticate, see X.509.

    Use your connection string to instantiate a MongoDB client in your application:

      // Copy the connection string provided by Atlas
      String uri = "mongodb+srv://<user>:<password>@<cluster-url>?retryWrites=true&w=majority";
      
      // Instantiate the MongoDB client with the URI
      MongoClient client = MongoClients.create(uri);
      
      // Copy the connection string provided by Atlas
      const uri = "mongodb+srv://<username>:<password>@<cluster-url>/test?retryWrites=true&w=majority";
      
      // Instantiate the MongoDB client with the URI
      const client = new MongoClient(uri, {
          useNewUrlParser: true,
          useUnifiedTopology: true
      });
      

      Retryable Writes and Reads

      Note

      Starting in MongoDB version 3.6 and with 4.2-compatible drivers, MongoDB retries both writes and reads once by default.

      Retryable Writes

      Use retryable writes to retry certain write operations a single time if they fail. If you copied your connection string from Atlas, it includes "retryWrites=true". If you are providing your own connection string, include "retryWrites=true" as a query parameter.

      Retrying writes exactly once is the best strategy for handling transient network errors and replica set elections in which the application temporarily cannot find a healthy primary node. If the retry succeeds, the operation as a whole succeeds and no error is returned. If the operation fails, it is likely due to:

      • A lasting network error, or
      • An invalid command.

      When an operation fails, your application needs to handle the error itself.

      Retryable Reads

      Read operations are automatically retried a single time if they fail starting in MongoDB version 3.6 and with 4.2-compatible drivers. No additional configuration is required to retry reads.

      Write and Read Concern

      You can tune the consistency and availability of your application using write concerns and read concerns. Stricter concerns imply that database operations wait for stronger data consistency guarantees, whereas loosening consistency requirements provides higher availability.

      Example

      If your application handles monetary balances, consistency is extremely important. You might use majority write and read concerns to ensure you never read from stale data or data that may be rolled back.

      Alternatively, if your application records temperature data from hundreds of sensors every second, you may not be concerned if you read data that does not include the most recent readouts. You can loosen consistency requirements and provide faster access to that data.

      Write Concern

      You can set the write concern level of your Atlas replica set through the connection string URI. Use a majority write concern to ensure your data is successfully written to your database and persisted. This is the recommended default and sufficient for most use cases. If you copied your connection string from Atlas, it includes "w=majority".

      When you use a write concern that requires acknowledgement, such as majority, you may also specify a maximum time limit for writes to achieve that level of acknowledgement:

      • The wtimeoutMS connection string parameter for all writes, or
      • The wtimeout option for a single write operation.

      Whether or not you use a time limit and the value you use depend on your application context.

      Important

      If you do not specify a time limit for writes and the level of write concern is unachievable, the write operation will hang indefinitely.

      Read Concern

      You can set the read concern level of your Atlas replica set through the connection string URI. The ideal read concern depends on your application requirements, but the default is sufficient for most use cases. No connection string parameter is required to use default read concerns.

      Specifying a read concern can improve guarantees around the data your application receives from Atlas.

      Note

      The specific combination of write and read concern your application uses has an effect on order-of-operation guarantees. This is called causal consistency. For more information on causal consistency guarantees, see Causal Consistency and Read and Write Concerns.

      Error Handling

      Invalid commands, network outages, and network errors that are not handled by retryable writes return errors. Refer to your driver’s API documentation for error details.

      For example, if an application tries to insert a document with a duplicate _id, your driver returns an error that includes:

        Unable to insert due to an error: com.mongodb.MongoWriteException:
        E11000 duplicate key error collection: <db>.<collection> ...
        
        {
            "name": : "MongoError",
            "message": "E11000 duplicate key error collection on: <db>.<collection> ... ",
            ...
        }
        

        Without proper error handling, an error may block your application from processing requests until it is restarted.

        Your application should handle errors without crashing or side effects. In the previous example of an application inserting a duplicate _id, that application could handle errors as follows:

        // Declare a logger instance from java.util.logging.Logger
        private static final Logger LOGGER = ...
        ...
        try {
            InsertOneResult result = collection.insertOne(new Document()
                .append("_id", 1)
                .append("body", "I'm a goofball trying to insert a duplicate _id"));
        
            // Everything is OK
            LOGGER.info("Inserted document id: " + result.getInsertedId());
        
        // Refer to the API documentation for specific exceptions to catch
        } catch (MongoException me) {
            // Report the error
            LOGGER.severe("Failed due to an error: " + me);
        }
        
        ...
        collection.insertOne({
            _id: 1,
            body: "I'm a goofball trying to insert a duplicate _id"
        })
        .then(result => {
            response.sendStatus(200) // send "OK" message to the client
        },
        err => {
            response.sendStatus(400); // send "Bad Request" message to the client
        });
        

        The insert operation in this example throws a “duplicate key” error the second time it’s invoked because the _id field must be unique. The error is caught, the client is notified, and the app continues to run. The insert operation fails, however, and it is up to you to decide whether to show the user a message, retry the operation, or do something else.

        You should always log errors. Common strategies for further processing errors include:

        • Return the error to the client with an error message. This is a good strategy when you cannot resolve the error and need to inform a user that an action cannot be completed.
        • Write to a backup database. This is a good strategy when you cannot resolve the error but don’t want to risk losing the request data.
        • Retry the operation beyond the single default retry. This is a good strategy when you can solve the cause of an error programmatically, then retry it.

        You must select the best strategies for your application context.

        Example

        In the example of a duplicate key error, you should log the error but not retry the operation because it will never succeed. Instead, you could write to a fallback database and review the contents of that database at a later time to ensure that no information is lost. The user doesn’t need to do anything else and the data is recorded, so you can choose not to send an error message to the client.

        Planning for Network Errors

        Returning an error can be desirable behavior when an operation would otherwise hang indefinitely and block your application from executing new operations. You can use the maxTimeMS method to place a time limit on individual operations, returning an error for your application to handle if that time limit is exceeded.

        The time limit you place on each operation depends on the context of that operation.

        Example

        If your application reads and displays simple product information from an inventory collection, you can be reasonably confident that those read operations only take a moment. An unusually long-running query is a good indicator that there is a lasting network problem. Setting maxTimeMS on that operation to 5000, or 5 seconds, means that your application receives feedback as soon as you are confident there is a network problem.

        Test Failover

        In the spirit of chaos testing, Atlas will perform replica set elections automatically for periodic maintenance and certain configuration changes.

        To check if your application is resilient to replica set elections, test the failover process by simulating a failover event.

        Resilient Example Application

        The following example application brings together the recommendations for building resilient applications.

        The application is a simple user records API that exposes two endpoints on http://localhost:3000:

        Method Endpoint Description
        GET /users Gets a list of user names from a users collection.
        POST /users Requires a name in the request body. Adds a new user to a users collection.

          Note

          The following server application uses NanoHTTPD and json which you need to add to your project as dependencies before you can run it.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47
          48
          49
          50
          51
          52
          53
          54
          55
          56
          57
          58
          59
          60
          61
          62
          63
          64
          65
          66
          67
          68
          69
          70
          71
          72
          73
          74
          75
          76
          77
          78
          79
          80
          81
          82
          83
          84
          85
          86
          87
          88
          89
          90
          91
          92
          // File: App.java
          
          import java.util.Map;
          import java.util.logging.Logger;
          
          import org.bson.Document;
          import org.json.JSONArray;
          
          import com.mongodb.MongoException;
          import com.mongodb.client.MongoClient;
          import com.mongodb.client.MongoClients;
          import com.mongodb.client.MongoCollection;
          import com.mongodb.client.MongoDatabase;
          
          import fi.iki.elonen.NanoHTTPD;
          
          public class App extends NanoHTTPD {
              private static final Logger LOGGER = Logger.getLogger(App.class.getName());
          
              static int port = 3000;
              static MongoClient client = null;
          
              public App() throws Exception {
                  super(port);
          
                  // Replace the uri string with your MongoDB deployment's connection string
                  String uri = "mongodb+srv://<user>:<password>@<cluster-url>?retryWrites=true&w=majority";
                  client = MongoClients.create(uri);
          
                  start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
                  LOGGER.info("\nStarted the server: http://localhost:" + port + "/ \n");
              }
          
              public static void main(String[] args) {
                  try {
                      new App();
                  } catch (Exception e) {
                      LOGGER.severe("Couldn't start server:\n" + e);
                  }
              }
          
              @Override
              public Response serve(IHTTPSession session) {
                  StringBuilder msg = new StringBuilder();
                  Map<String, String> params = session.getParms();
          
                  Method reqMethod = session.getMethod();
                  String uri = session.getUri();
          
                  if (Method.GET == reqMethod) {
                      if (uri.equals("/")) {
                          msg.append("Welcome to my API!");
                      } else if (uri.equals("/users")) {
                          msg.append(listUsers(client));
                      } else {
                          msg.append("Unrecognized URI: ").append(uri);
                      }
                  } else if (Method.POST == reqMethod) {
                      try {
                          String name = params.get("name");
                          if (name == null) {
                              throw new Exception("Unable to process POST request: 'name' parameter required");
                          } else {
                              insertUser(client, name);
                              msg.append("User successfully added!");
                          }
                      } catch (Exception e) {
                          msg.append(e);
                      }
                  }
          
                  return newFixedLengthResponse(msg.toString());
              }
          
              static String listUsers(MongoClient client) {
                  MongoDatabase database = client.getDatabase("test");
                  MongoCollection<Document> collection = database.getCollection("users");
          
                  final JSONArray jsonResults = new JSONArray();
                  collection.find().forEach((result) -> jsonResults.put(result.toJson()));
          
                  return jsonResults.toString();
              }
          
              static String insertUser(MongoClient client, String name) throws MongoException {
                  MongoDatabase database = client.getDatabase("test");
                  MongoCollection<Document> collection = database.getCollection("users");
          
                  collection.insertOne(new Document().append("name", name));
                  return "Successfully inserted user: " + name;
              }
          }
          

          Note

          The following server application uses Express, which you need to add to your project as a dependency before you can run it.

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47
          48
          49
          50
          51
          52
          53
          54
          55
          56
          57
          58
          59
          60
          61
          62
          63
          64
          const express = require('express');
          const bodyParser = require('body-parser');
          
          // Use the latest drivers by installing & importing them
          const MongoClient = require('mongodb').MongoClient;
          
          const app = express();
          app.use(bodyParser.json());
          app.use(bodyParser.urlencoded({ extended: true }));
          
          // Use the Atlas-provided connection string
          // with retryable writes,
          // majority write concern & default read concern
          const uri = "mongodb+srv://<username>:<password>@cluster0-111xx.mongodb.net/test?retryWrites=true&w=majority";
          
          const client = new MongoClient(uri, {
              useNewUrlParser: true,
              useUnifiedTopology: true
          });
          
          // ----- API routes ----- //
          app.get('/', (req, res) => res.send('Welcome to my API!'));
          
          app.get('/users', (req, res) => {
              const collection = client.db("test").collection("users");
          
              collection
              .find({})
              // In this example, 'maxTimeMS' throws an error after 5 seconds,
              // alerting the application to a lasting network outage
              .maxTimeMS(5000)
              .toArray((err, data) => {
                  if (err) {
                      // Handle errors in your application
                      // In this example, by sending the client a message
                      res.send("The request has timed out. Please check your connection and try again.");
                  }
                  return res.json(data);
              });
          });
          
          app.post('/users', (req, res) => {
              const collection = client.db("test").collection("users");
              collection.insertOne({ name: req.body.name })
              .then(result => {
                  res.send("User successfully added!");
              }, err => {
                  // Handle errors in your application
                  // In this example, by sending the client a message
                  res.send("An application error has occurred. Please try again.");
              })
          });
          // ----- End of API routes ----- //
          
          app.listen(3000, () => {
              console.log(`Listening on port 3000.`);
              client.connect(err => {
                  if (err) {
                      console.log("Not connected: ", err);
                      process.exit(0);
                  }
                  console.log('Connected.');
              });
          });