title: Firebase: Flutter: Optimizing for stability: many Transactions vs. Batch writes vs. Set calls
body: <p>A few months ago, a glorious soul here taught me about transactions. I may have gone a little overboard thinking they were they best thing since sliced bread. The problem they solved was obvious, guaranteed concurrent writes on a single doc. However, I've noticed already with as little as three closely timed function triggers that I produced the dreaded:<br><br> ------------------------"10 ABORTED: Too much contention on these documents."...-------------------------</p>
  
  <p>Optimizing for stability, my question is: Would it be best practice to use a mixed bag of these write calls for different situations? For example: if a cloud function is writing to a location where I do not expect contention, should it just be a set call? Instead of 4 transactions to various locations, should I use a batch? </p>
  
  <p>Reading the Firebase limitations I assumed I was in the clear with max 60w/doc/sec. However, I've learned now that Transactions can timeout AND only try to write 5 times.</p>
  
  <p>Some background on the app and the contention error: <br>
  - It's a basic social media app.<br>
  - The contention error came from making three posts in close succession from a single user.<br>
  - Each post triggers a cloud function that does several transactions to link the post to appropriate places. i.e. followers, feed, groups, sends notifications, and sets activity feed docs for each follower.</p>
  
  <p>Side question: Am I wrongly understanding that firebase can handle an app with this level of activity?</p>
  
  <p>EDIT: I was aware of these firebase limitations early on and did my best work to keep documents and collections spread apart appropriately.</p>
  
  <p>CODE EDIT: index.js: adminPostReview is the specific function to throw the error (did the best I could to simplify).<br>
  The specific transaction to throw the error, I believe, is the call to transactionDayIndexAdd(). </p>
  
  <pre><code>function transactionDelete(docRef) {
      return db.runTransaction(async t =&gt; {
          var doc = await t.get(docRef);
          if (doc.exists)
              t.delete(docRef);
      })
  }
  
  // THIS FUNCTION. Is it bad to read and set two documents?
  function transactionDayIndexAdd(docRef, dayPosted, postId, userId) {
      return db.runTransaction(async (t) =&gt; {
          var postMap = {};
          const doc = await t.get(docRef.doc(dayPosted));
          if (doc.exists) {
              postMap = doc.data().pids;
          } else {
              const indexDoc = await t.get(docRef.doc('index'));
              var newIndex = indexDoc.exists ? indexDoc.data().index : {};
              newIndex[dayPosted] = true;
              t.set(docRef.doc('index'), { 'index': newIndex });
          }
          postMap[postId] = userId;
          t.set(doc.ref, { 'pids': postMap });
      })
  }
  
  exports.adminPostReview = functions.firestore
      .document('/adminPostReview/{postId}')
      .onUpdate(async (change, context) =&gt; {
          const postId = context.params.postId;
          const userId = change.before.data().ownerId;
          const approvedMaks = change.after.data().approvedMaks;
          const approvedRita = change.after.data().approvedRita;
          var promises = [];
          if (approvedMaks == false || approvedRita == false) {
              promises.push(transactionDelete(db.collection('posts').doc(userId).collection('userPosts').doc(postId)));
          }
          else if (approvedMaks == true || approvedRita == true) {
              var newPost = change.after.data();
              promises.push(postLive(newPost));
          }
          if (approvedMaks != null || approvedRita != null) {
              promises.push(transactionDelete(db.collection('activityFeed').doc(MAKS_ID).collection('feedItems').doc(`${postId}_review`)));
              promises.push(transactionDelete(db.collection('activityFeed').doc(RITA_ID).collection('feedItems').doc(`${postId}_review`)));
          }
      });
  
  async function postLive(newPost) {
      const userId = newPost.ownerId;
      const postId = newPost.postId;
      const dayPosted = newPost.dayPosted;
  
      var postToFeed = newPost.postToFeed;
      var postToGroups = newPost.postToGroups;
      newPost.approved = true;
      delete newPost.postToFeed;
      delete newPost.postToGroups;
  
      var batch = db.batch();
      var promises = [];
      if (postToFeed == true) {
          batch.set(
              db.collection('posts').doc(userId).collection('userPosts').doc(postId),
              newPost
          );
          batch.update(
              db.collection('userActivity').doc(userId),
              'numPosts',
              admin.firestore.FieldValue.increment(1),
          )
          promises.push(batch.commit());
          promises.push(transactionDayIndexAdd(db.collection("feedRandom"), dayPosted, postId, userId));
          var querySnap = await db.collection('followers')
              .doc(userId)
              .collection('userFollowers')
              .get();
          querySnap.docs.forEach(async follower =&gt; {
              promises.push(transactionDayIndexAdd(
                  db.collection('feedFollowing').doc(follower.id).collection('feedItems'),
                  dayPosted, postId, userId));
              promises.push(transactionSet(db.collection('activityFeed').doc(follower.id)
                  .collection('feedItems').doc(postId),
                  {
                      media1Url: newPost.media1Url,
                      media2Url: newPost.media2Url,
                      postId: newPost.postId,
                      timestamp: newPost.timestamp,
                      type: 'newFollowingPost',
                      userId: userId,
                      userProfileImg: newPost.ownerProfileImg,
                      username: newPost.username,
                      displayName: newPost.displayName,
                  }
              ));
              if (follower.data().notificationToken != null) {
                  const payload = {
                      notification: {
                          title: 'Someone you follow made a new post!',
                          body: `${newPost.username} has a new post.`
                      },
                      data: {
                          click_action: "FLUTTER_NOTIFICATION_CLICK",
                          vestiq_type: 'newFollowingPost',
                          vestiq_uid: follower.id,
                          vestiq_fid: userId,
                          vestiq_pid: postId,
                          vestiq_displayName: newPost.displayName,
                          vestiq_photoUrl: newPost.ownerProfileImg,
                          vestiq_username: newPost.username,
                      }
                  };
                  var user = await db.collection('users').doc(follower.id).get();
                  if (user.data().notificationOp3 == true)
                      promises.push(pushNotification(follower.data().notificationToken, payload));
              }
          });
          if (postToGroups != null &amp;&amp; postToGroups.length &gt; 0) {
              promises.push(pushGroupPosts(postToGroups, userId, postId, newPost));
              return Promise.all(promises);
          } else return Promise.all(promises);
      }
      else if (postToGroups != null &amp;&amp; postToGroups.length &gt; 0) {
          promises.push(pushGroupPosts(postToGroups, userId, postId, newPost));
          return Promise.all(promises);
      }
  }
  
  async function pushGroupPosts(postToGroups, userId, postId, newPost) {
      var groupBatch = db.batch();
      postToGroups.forEach((gid) =&gt; {
          groupBatch.set(
              db.collection('groups').doc(gid).collection('posts').doc(postId),
              newPost,
          );
          groupBatch.set(
              db.collection('usersGroupPosts').doc(userId).collection(gid).doc(postId),
              { 'gid': gid, 'postId': postId },
          );
      });
      return push(groupBatch.commit());
  }
  </code></pre>