In this lesson, you will learn how to guarantee that multiple database operations within a single HTTP request succeed or fail as a single atomic unit.
The Challenge of Distributed Operations
In complex Node.js applications, a single incoming request often triggers multiple database writes across different services or modules. For example, when a user completes a checkout, your system might simultaneously deduct inventory, create an order record, and log a transaction history.
If the server crashes or a database constraint fails midway through these operations, you risk leaving your database in an inconsistent "partial" state—where the money is deducted but no order is created.
Implementing Request-Scoped Transactions
To prevent this, we use a Request-Scoped Transaction. The core idea is to create a single database "transaction object" at the very start of the request lifecycle and inject that specific object into every service method that performs a write.
By using the Scoped Dependency Resolution techniques we covered earlier, we can ensure that every part of the request processing pipeline shares the exact same database transaction context.
Code Illustration
Imagine a middleware that wraps your request in a transaction:
app.use(async (req, res, next) => {
// 1. Start a database transaction
const transaction = await db.beginTransaction();
// 2. Attach it to the request scope so services can access it
req.transaction = transaction;
try {
await next();
// 3. Commit only if all operations succeed
await transaction.commit();
} catch (error) {
// 4. Roll back everything if any part fails
await transaction.rollback();
res.status(500).send("Operation failed; state reverted.");
}
});
When your InventoryService or OrderService performs an operation, it checks req.transaction. If it exists, the service executes its SQL commands using that specific transaction object instead of the default database connection.
This ensures that the database treats every service call as part of one "all-or-nothing" operation. If an error occurs in the final step, the rollback() command cleans up all previous changes automatically, keeping your data perfectly synchronized without needing complex manual cleanup logic in every service.
Key Takeaway: By binding a single transaction context to the lifecycle of the HTTP request, you ensure atomicity across disparate services, preventing partial data updates and maintaining system integrity.