Why you should avoid using Mongoose .save() method for updates

Akshay Jain
4 min readMar 5, 2022

Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. It makes it extremely easy to interact with MongoDB for server-side applications (for example, ones built with express) by manipulating mongo documents like JavaScript objects.

For example, you have a wallet management system (a MongoDB + Express.js API) with a wallet model that looks like this.

PS: Rupees (Rs) is the Indian Currency like USD

Now, you want to enhance the API by building an endpoint that deducts 10 Rupees from the wallet. (a nasty system, but as simple as that). You’ll probably do something like this.

This is a right way to do the task, however, it is problematic.

Reason? Atomicity

Atomicity in simple terms means that any entity (normal variables, objects, etc.) must be updated in one single operation. That is, there are no midways.

In the above example, we do something like this

As you can see that the update in wallet amount is a 4 step process

  1. Getting Wallet from Database.
  2. Saving data in `foundWallet` variable.
  3. Mutating the variable (i.e reducing amount by 10).
  4. Saving the mutated object into the Database.

As you can see the update operation is not atomic in nature, you’ll have 2 DB calls (one to get wallet another to update)

Let’s see where it will NOT work.

The Idea is simple what if database is changed before you call .save()?

Pictorially, let us assume that each operation takes place at time T(i), where T(i)<T(i+1), example (T1 (Time 1) < T2 (Time 2))

Operations on Database Server, Note at time T5 we save value as 80, i.e 20 Rs, deducted from 100, however at time T6 our Operation at T1 is completed overwriting value to 90.

Did you see what happened?

At Time T1 we withdrew 10 Rs from our wallet with 100 amount, however that transaction was not completed Instantaneously. Meanwhile while transaction was going on at T3, I withdrew another 20 Rs from my account which updated my balance to Rs 80, after the withdrawal my previous transaction (10 Rs one) completed updating the balance to 90, since we mutated the object non atomically.

Now, you might be happy for this mishap but the bank won’t be :)

This is how you can replicate the problem via code

Psst- Don’t worry I’ve provided the entire working code later.

You can call route PUT /:id/withdraw10 first (don’t wait for its completion) then call Route PUT /:id/withdraw20 to check it yourself.

How to resolve this?

Instead of using .save(), write queries, using `updateOne` method (I know it sucks but on the bright side bank is not angry with you anymore.)

We used $inc operator provided by MongoDB, to update directly at Time T1 and T2. This ensures our wallet remains upto date.

The flow in this case looks like this

Note, By Arrows I am only expressing the DB calls, with mongoose update method also returns a response. So these calls are not to be confused with HTTP requests.

Conclusion

The code above was a plain example why atomicity is important for us. In real life scenarios there can be plenty of cases where mutation via .save() might lead to problems.

Examples include:

Instead of calling the timeout function, you are doing manipulation on bigdata (Example Bubble sort on an array of 1,000,000 entries since you too are not in a habit of grinding Leetcode)

Realtime collaboration where multiple users actively mutate a single entity very rapidly example google docs. Such problems fall under transactions and there is a plethora of problems which can be addressed with the same idea. Hope you enjoyed the blog!

The complete working set of code can be found at GitHub here.

--

--

Akshay Jain

Software engineer, fascinated by backend technologies. Goes by i-rebel-aj on most social platforms