How to Create an ACID Transaction in Mongoose

How to Create an ACID Transaction in Mongoose

A transaction, in the context of database systems, is a single unit of work. It either has to be completed fully or not completed at all to leave the database in a consistent state.

For example,

In an e-commerce application, when a new order is created, you may need to:

  • reduce the inventory count for the items,

  • create a new order,

If you missed one of these steps, your database will be inconsistent. E.g., The inventory may be reduced and the order was not created. This is a discrepancy that will affect the database integrity.

This is the problem that transactions are required to solve.

In this blog, I will cover:

  1. What is a transaction?

  2. How to make a transaction ACID compliant

  3. How to create an ACID transaction with Mongoose

What is a Transaction?

A transaction is a group of operations that read and write data. The transaction is deemed successful only when all the operations are successful. These operations can include one or more records.

The results of all the operations in a successful transaction are committed to the database and can not be reverted. If there's an error, the transaction will be aborted and all the changes that were already made would be reverted.

In the example above, if the inventory is reduced then an error occurs when adding the order, the transaction will be stopped and the inventory will be reverted to the initial count.

So what makes a transaction successful?

What Makes a Transaction ACID?

ACID is an acronym that stands for Atomicity, Consistency, Isolation and Durability.

These are the core characteristics of a successful transaction. They are used to maintain data integrity and consistency.

a) Atomicity

Atomicity means that the transaction is either completely fulfilled or not fulfilled at all. If an error occurs during the transaction, it will be aborted and the changes reverted.

For example, in the e-commerce application, the inventory count is reduced and the order is created. These two operations need to be successful for the transaction to completed and committed to the database.

In the image above, the transaction is aborted after the inventory count is reduced due to a system failure. The inventory count will be reduced but the orders will not be updated.

When the system is running again, the operations will be executed and the inventory count will be reduced again. The inventory count will be updated twice leading to an inconsistency.

In an ACID-compliant transaction, when the system fails, all the changes are reverted to avoid inconsistency. Therefore, the inventory count will only be updated when the system is running.

b) Consistency

Consistency means that the database is left in a consistent state after the transaction is completed.

So what does it mean for a database to be consistent?

The data before the transaction and the data after the transaction are equal.

For example,

Anne sends 300 to Jane, who has a balance of 1000, from her balance of 2000.

In a situation where money is deducted from Anne's account but not added to Jane's account, the database will be inconsistent. i.e,

The data before the transaction is: 1000 + 2000 = 3000.

The data after the transaction is: 1000 + 1700 = 2700.

In a successful transaction,

the data before the transaction is: 1000 + 2000 = 3000.

the data after the transaction will be: 1300 + 1700 = 3000.

c) Isolation

Isolation means that the operations in a transactions are performed independently and should not affect each other.

All operations in a transaction are independent and cannot access each other's results.

For example,

An expense tracker creates a new expense and deducts the amount from the specified account. The tracker also creates an object that contains these details.

The transaction, in the image above, will not be successful because the transaction will require the id of the expense. The transaction operation is dependent on the expense operation.

The transaction operation cannot access the results of adding the expense because they have not been committed which will make the transaction abort.

We can separate the operations into two different transactions. This means that the changes made by the expense operation will be committed and can be used to create the new transaction.

d) Durability

Durability means that the results of a successful transaction are persisted even when there's a system failure.

Once a successful transaction is committed, the effects of the operations are updated in the database and cannot be deleted or removed during a system failure.

How to Create an ACID Transaction with Mongoose

To illustrate how to create an ACID transaction, I will be using an e-commerce application where I have two models, Inventory and Order.

  1. Start a session

MongoDB and Mongoose have sessions that are an essential part of transactions. Sessions allows us to group operations and track their execution within a transaction.

Sessions provide a space for transactions to be executed. The session keeps the results of the operations saved until the transaction is committed.

// using the default database connection
const session = mongoose.startSession();

// specifiying a database connection
const mongoURL = "mongodb:srv//localhost:2700";
const dbConn = await mongoose.createConnection(mongoURL).asPromise();
const dbSession = await dbConn.startSession();
  1. Start a transaction

This marks the beginning of the transaction and every operation after this statement will be part of the transaction.

You can start a transaction using the session created.

session.startTransaction();
  1. Operations

Once the transaction has been started, you can list all the operations that are required in your transaction.

try{
    // reducing the inventory count
    for(let item in order.items){
        await Inventory.updateOne({
            name: item.name
         }, {
            $inc: {
                quantity: -item.quantity
            }
        })
    }

    // creating the order
    await Order.insertOne({
        customer_id: "38h1hf84bf8y5hf88u5nf8u58uf"
        timestamp: new Date()
        items: order.items
    });

}catch(err){
    console.error("Failed: " + err);
}
  1. Commit the transaction

Committing the transaction will save the changes and make them permanent. This statement also ends the current transaction.

try{
    // operations
    session.commitTransaction();
}catch(err){}
  1. End the session

Once you're done with the transaction, you have to end the session.

try{
    // operations
    await session.commitTransaction();
    session.endSession();
}catch(err){}

This transaction reduces the inventory count and creates a new order.

What happens if we need to create a separate model for the products in an order? This model will require the product name, quantity and the order id.

The products model is dependent on the order operation. In order for the transaction to remain ACID-compliant, this model needs to be updated in a separate transaction.

A session can be used for multiple transactions so the two transactions can be carried out in one session.

Here's the full code for these transactions:

function createOrder(order){
    const session = mongoose.startSession();
    session.startTransaction();

    try{
        // reducing the inventory count
        for(let item in order.items){
            await Inventory.updateOne({
                name: item.name
             }, {
                $inc: {
                    quantity: -item.quantity
                }
            })
        }

        // creating the order
        await Order.insertOne({
            customer_id: "38h1hf84bf8y5hf88u5nf8u58uf"
            timestamp: new Date()
        });

        await session.commitTransaction();

        // starting a new transaction
        session.startTransaction();

        // finding the order id
        let latestOrder = Order.find({}).sort({_id:-1}).limit(1)[0];
        let orderId = latestOrder._id;

        // adding the products
        for(let item in order.items){
            await OrderProducts.insertOne({
                name: item.name
                quantity: item.quantity
                order: orderId
            })
        }

        await session.commitTransaction();
        session.endSession();

    }catch(err){
        console.error("Failed: " + err);
    }

}

Conclusion

  • A transaction is a single unit of operations that have to be fully completed or not completed at all.

  • Transactions maintain database integrity and consistency.

  • A successful transaction has ACID properties.

  • Atomicity means that the transaction is either fully executed or not executed at all.

  • Consistency means that the transaction must leave the database in a consistent state.

  • Isolation means that all the operations in a transaction should be independent.

  • Operations in a transactions cannot access each other's results.

  • Durability means that the results of a committed transaction are permanent.