In a basic CRUD tutorial, creating a product looks like a single line: await Product.create(req.body).
In a real e-commerce architecture, publishing a product actually involves four distinct database writes:
- Insert the parent
Productdocument. - Insert eight child
ProductVariantdocuments referencing the parent ID. - Update category product counts in the
Categorycollection. - Record an audit trail in the
ActivityLogcollection.
Now, imagine step 1 and 2 succeed, but step 3 throws a database timeout error.
Without atomic transactions, you now have a parent product and eight variant documents orphaned in your database, but missing category counts and missing audit trails.
Your database is officially out of sync.
The ACID Transaction Pattern in Mongoose
To ensure that either all four operations succeed together or none of them execute at all, we wrap multi-document mutations in MongoDB sessions:
const session = await mongoose.startSession();
session.startTransaction();
try {
// 1. Create parent product
const [product] = await Product.create([productData], { session });
// 2. Insert variant matrix
await ProductVariant.insertMany(
variants.map((v) => ({ ...v, productId: product._id })),
{ session }
);
// 3. Log activity trail
await ActivityLog.create([
{ action: "PRODUCT_CREATED", targetId: product._id, admin: req.user._id }
], { session });
// Commit all writes atomically
await session.commitTransaction();
return product;
} catch (error) {
// If anything fails, rollback every single write!
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
The Takeaway
Data consistency is not something you fix after the fact. It must be built into the mutation layer.
Whenever a user action touches more than one database collection, treat it as a single atomic transaction. Either everything succeeds, or the database rolls back cleanly.