API with NestJS #46. Managing transactions with MongoDB and Mongoose

JavaScript MongoDB NestJS TypeScript

This entry is part 46 of 48 in the API with NestJS

While working with databases, keeping the integrity of the data is crucial. For example, imagine transferring money from one bank account to another. To do that, we need to perform two separate actions. First, we withdraw the amount from the first bank account. Then, we add the same amount to the second account.

If the second operation fails for whatever reason while the first succeeds, we end up with an invalid state of the database. We need the above operations to all succeed or all fail together. We can accomplish that with transactions.

ACID properties

A transaction to be valid needs to have the following properties. Together, they form the ACID acronym:

Atomicity

Operations in the transaction are a single unit. Therefore, it either fully succeeds or fails together.

Consistency

The transaction moves the database from one valid state to the next.

Isolation

The isolation property ensures that multiple transactions can occur concurrently, resulting in a valid database state. To better understand that, let’s continue the example with the banking transaction from above. Another transaction should see the funds in one account or the other, but not in both.

Durability

Once the changes from a transaction are committed, they should survive permanently.

Transactions in MongoDB and Mongoose

Fortunately, MongoDB is equipped with support for multi-document transactions since version 4.0. We can tell the database that we do a transaction, and it keeps track of every update we make. If something fails, then the database rolls back all our updates. The above requires the database to do extra work making notes of our updates and locking the involved resources. Other clients trying to perform operations on the data might be stuck waiting for the transaction to complete. Therefore, this is something to watch out for.

Running a replica set

Transactions with MongoDB only work with a replica set, a group of MongoDB processes that maintain the same data set. In this series, we’ve been using docker-compose to run MongoDB for us. We can either run a replica set locally with docker or use MongoDB atlas. For this article, I’m doing the latter.

If you want to run a replica set, check out this page on Stackoverflow.

Deleting a user

Let’s implement a feature of deleting a user. When we remove users from the database, we also want to delete all posts they wrote.

users.service.ts

To do the above, we also need to define the in our .

posts.service.ts

The shortcoming of the above code is that the method might succeed partially. When this happens, we delete the user, but the posts are left in the database without the author. We can deal with the above issue by defining a transaction.

To start a transaction, we need to access the connection we’ve established with MongoDB. To do that, we can use the decorator:

users.service.ts

Controlling the transaction

There are two ways of working with transactions with Mongoose. To have full control over it, we can call the  method:

When we indicate that everything worked fine, we need to call . This writes our changes to the database.

If we encounter an error, we need to call to indicate that we want to discard the operations we’ve performed so far. Once we’re done with the transaction, we need to call the method.

To indicate that we want to perform an operation within a given session, we need to use the method.

users.service.ts

Still, there is an important issue with the above code. Although we’ve deleted the user within a transaction, we didn’t do that when removing posts. To delete posts within a session, we need to modify the function:

posts.service.ts

By adding the optional argument to the method, we can delete posts within a transaction. Let’s use it:

users.service.ts

If removing the posts fail for some reason, the user is not deleted from the database either. Thanks to that, the whole operation either succeeds as a whole or fails completely.

A simpler way of using transactions

Instead of controlling every step of the transaction manually, we can use the helper.

users.service.ts

Please notice that we no longer need to call , , and . We still are required to end the session with the method, though.

Summary

In this article, we’ve gone through transactions in MongoDB by describing their principles and use-cases. We’ve also implemented them into our application with Mongoose. It is definitely worth it to understand transactions because they can increase the reliability of our application quite a lot.

Series Navigation<< API with NestJS #45. Virtual properties with MongoDB and MongooseAPI with NestJS #47. Implementing pagination with MongoDB and Mongoose >>
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
olp
olp
15 days ago

Thanks for the article, it is very informative as always.
Regarding the transactions, I wonder if it is possible to use a decorator to wrap a function in a transaction? I have tried on my side but could not get it to work.

olp
olp
11 days ago
Reply to  olp

Actually I found a way, I set the mongoose session inside de request object and use a custom decorator to retrieve it if needed.