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.
// 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
updateOne
with the session to deduct the amount from the sender's account.Add to Receiver: Similarly, add the amount to the receiver's account using
updateOne
with 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:
accountRoutes.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
const 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
await 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.
Subscribe to my newsletter
Read articles from Aditya Kumar Gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Aditya Kumar Gupta
Aditya Kumar Gupta
Hi there! I'm Aditya, a passionate Full-Stack Developer driven by a love for turning concepts into captivating digital experiences. With a blend of creativity and technical expertise, I specialize in crafting user-friendly websites and applications that leave a lasting impression. Let's connect and bring your digital vision to life!