How to Perform Transactions in Mongoose: A Step-by-Step Guide
Transactions in MongoDB allow you to execute multiple operations in isolation, ensuring that all operations either complete successfully or none at all. This is particularly useful in scenarios like financial transactions, where consistency is crucial. In this blog post, we'll explore how to implement transactions in Mongoose, a popular ODM for MongoDB, using an example of a money transfer between accounts.
Setting Up the Environment
Before diving into the code, ensure you have the following set up:
-
Node.js and Express: For building the server.
-
Mongoose: To interact with MongoDB.
-
MongoDB: A running instance of MongoDB.
Extending Express Request
First, extend the Express Request type to include a userId property. This is useful for identifying the user making the request.
typescript// Extend Express Request type once in your project (e.g. types/express.d.ts) declare global { namespace Express { interface Request { userId?: string; } } }
Implementing the Transfer Route
Here's a step-by-step guide to implementing a transfer route using transactions:
-
Start a Session: Begin by starting a session with
mongoose.startSession(). This session will be used to manage the transaction. -
Start a Transaction: Use
session.startTransaction()to initiate the transaction. -
Check Target Account: Verify if the target account exists. If not, abort the transaction and end the session.
-
Check Sender Balance: Ensure the sender has sufficient balance. If not, abort the transaction and end the session.
-
Deduct from Sender: Use
updateOnewith the session to deduct the amount from the sender's account. -
Add to Receiver: Similarly, add the amount to the receiver's account using
updateOnewith the session. -
Commit the Transaction: If all operations are successful, commit the transaction with
session.commitTransaction(). -
Handle Errors: In case of any errors, abort the transaction and end the session.
Here's the complete code for the transfer route:
javascriptaccountRoutes.post("/transfer", userMiddleware, async (req: Request, res: Response) => { const session = await mongoose.startSession(); session.startTransaction(); try { const { transferAccount, amount } = req.body; // Check if target account exists const accountExist = await userModel.findById(transferAccount).session(session); if (!accountExist) { await session.abortTransaction(); await session.endSession(); return res.status(400).json({ message: "User Account does not exist" }); } // Check sender balance const balanceCheck = await accountModel.findOne({ userid: req.userId }).session(session); if (!amount || amount > (balanceCheck?.balance ?? 0)) { await session.abortTransaction(); await session.endSession(); return res.status(400).json({ message: "Insufficient Balance" }); } // Deduct from sender await accountModel.updateOne( { userid: req.userId }, { $inc: { balance: -amount } } ).session(session); // Add to receiver await accountModel.updateOne( { userid: transferAccount }, { $inc: { balance: amount } } ).session(session); // Commit transaction await session.commitTransaction(); await session.endSession(); return res.status(200).json({ amount, message: "Send Successfully" }); } catch (error) { await session.abortTransaction(); await session.endSession(); console.error("Transfer error:", error); return res.status(500).json({ message: "Something went wrong", error }); } });
Transaction Functions
1. startTransaction()
-
Begins a new transaction on the session.
-
After this, all queries run under that session are part of the transaction.
Use it when:
You’re about to perform multiple dependent operations that should succeed or fail together.
2. commitTransaction()
-
Saves (commits) all the changes made during the transaction.
-
Once committed, changes are permanent in the database.
Use it when:
All operations inside the transaction are successful.
Example: After deducting from A and crediting B, commit to finalising the transfer.
3. abortTransaction()
-
Cancels (rolls back) the transaction.
-
All changes made during the transaction are undone as if nothing had happened.
Use it when:
Any operation fails, or the validation doesn’t pass.
Example: If A doesn’t have enough balance, abort to cancel the deduction.
4. endSession()
-
Ends the session.
-
It doesn’t commit or abort by itself; it just releases the resources.
-
You should always call it after
commitTransaction()orabortTransaction()to clean up.
Use it when:
At the very end, after committing or aborting, close the session.
Typical Flow in a Money Transfer
-
Start Session & Transaction
jsonconst session = await mongoose.startSession() session.startTransaction() -
Do some operations
-
Deduct from sender
-
Add to receiver
-
-
Check conditions
-
If balance is insufficient →
abortTransaction() -
If all okay →
commitTransaction()
-
-
End Session
jsonawait session.endSession()
Conclusion
Implementing transactions in Mongoose with MongoDB ensures data consistency and integrity, especially in critical operations like financial transactions. By following the steps outlined above, you can effectively manage transactions in your applications, providing a reliable and robust user experience.