DEV Community

Cover image for Handling Transactions via Sessions in Mongoose
Harsh Srivastav
Harsh Srivastav

Posted on

Handling Transactions via Sessions in Mongoose

Imagine you're working on a backend of a fintech startup. This piece of code handles a fund transfer functionality, and it is mission critical that the transactions are consistent.

One way is the basic approach, you increase the balance in recipient's account, and reduce the balance in sender's account, two API calls. But what if, the DB crashed in middle, or some network issue occurred! This creates a state called Database Inconsistency, and it's where we need Transactions.

Transactions ensure data consistency by guaranteeing that a series of database operations are treated as a single unit. This means either all operations succeed, or none of them do, preventing data inconsistencies.

Database transactions adhere to the ACID properties:

  • Atomicity: All operations are treated as one.

  • Consistency: The database transitions from one valid state to another.

  • Isolation: Transactions are isolated from each other, preventing conflicts.

  • Durability: Once committed, changes persist even in case of system failures.

In mongoose, this especially becomes much easier to work with. In our case, we can specify when we started a session, using:

const session = await mongoose.startSession();
Enter fullscreen mode Exit fullscreen mode

Now, we have two ways of approaching:

  • Either do an API call, and check if it resolved correctly, then do a session.commitTransaction(). This creates an awful lot of commit statements, violating the DRY principle.

  • We can do something called session.withTransaction()

accountRoute.post("/transfer", authMiddleware, async (req,res) => {
    const fromAcntUserId = req.userId
    const toAcntUserId = req.body.to
    const txnAmount = parseFloat(req.body.amount)

    if (txnAmount <= 0) {
        return res.status(400).json({
            error: "Invalid Transaction Amount"
        })
    }
    const session = await mongoose.startSession();

    let fromAcnt, toAcnt;
    try {
        await session.withTransaction(async() => {
            fromAcnt = await Account.findOneAndUpdate(
                { userId: fromAcntUserId, balance: { $gte: txnAmount }},
                { $inc: { balance: - txnAmount } },
                {new : true, session}
            )

            if (!fromAcnt) {
                throw new Error("Insufficient funds or sender not found.")
            }
            toAcnt = await Account.findOneAndUpdate(
                { userId: toAcntUserId },
                { $inc: { balance: txnAmount } },
                {new : true, session}
            )

            if (!toAcnt) {
                throw new Error("Recipient account not found")
            }
        })

        res.status(200).json({
            message: "Transfer Successful",
            fromAccountBalance: fromAcnt.balance,
            toAccountBalance: toAcnt.balance
        })
    }
    catch (error) {
        return res.status(400).json({
            error: error.message
        })
    }
    finally {
        session.endSession()
    }

})
Enter fullscreen mode Exit fullscreen mode

This code first finds the sender's account (fromAcnt) and checks if their balance is sufficient. If not, it throws an error. Then, it finds the recipient's account (toAcnt) and updates their balance.

The beauty of second approach is, only if all the steps mentioned in the await session.withTransaction(async () => {... are executed successfully, is the whole transaction committed fully. If any one of it fails, the whole transaction is rolled back.

At last, after transaction has been either rolled back or committed, session.endSession(), to close this session.

Top comments (2)

Collapse
 
respect17 profile image
Kudzai Murimi

Thanks, for sharing with the community

l am happy to try it out too

Collapse
 
harsh32044 profile image
Harsh Srivastav

Glad to be of help!