Transactions in NoSQL

ACID in NoSQL

Traditional SQL databases guarantee ACID (Atomicity, Consistency, Isolation, Durability). NoSQL databases have varying levels of transaction support.

MongoDB Transactions

Multi-Document Transactions

const { MongoClient } = require('mongodb');

const client = new MongoClient('mongodb://localhost:27017');
await client.connect();

const session = client.startSession();

try {
  await session.withTransaction(async () => {
    const db = client.db('myapp');
    
    // Debit account
    await db.collection('accounts').updateOne(
      { _id: 'account1' },
      { $inc: { balance: -100 } },
      { session }
    );
    
    // Credit account
    await db.collection('accounts').updateOne(
      { _id: 'account2' },
      { $inc: { balance: 100 } },
      { session }
    );
    
    // Log transaction
    await db.collection('transactions').insertOne({
      from: 'account1',
      to: 'account2',
      amount: 100,
      timestamp: new Date()
    }, { session });
  });
  
  console.log('Transaction committed');
} catch (error) {
  console.error('Transaction aborted:', error);
} finally {
  await session.endSession();
}

Transaction Options

// Configure transaction
await session.withTransaction(
  async () => {
    // Transaction operations
  },
  {
    readPreference: 'primary',
    readConcern: { level: 'snapshot' },
    writeConcern: { w: 'majority' },
    maxCommitTimeMS: 30000
  }
);

.NET Transactions

using MongoDB.Driver;

public class TransactionService
{
    private readonly IMongoClient _client;
    
    public async Task TransferMoney(string fromId, string toId, decimal amount)
    {
        using (var session = await _client.StartSessionAsync())
        {
            session.StartTransaction();
            
            try
            {
                var db = _client.GetDatabase("myapp");
                var accounts = db.GetCollection<Account>("accounts");
                
                // Debit
                await accounts.UpdateOneAsync(
                    session,
                    a => a.Id == fromId,
                    Builders<Account>.Update.Inc(a => a.Balance, -amount)
                );
                
                // Credit
                await accounts.UpdateOneAsync(
                    session,
                    a => a.Id == toId,
                    Builders<Account>.Update.Inc(a => a.Balance, amount)
                );
                
                await session.CommitTransactionAsync();
            }
            catch
            {
                await session.AbortTransactionAsync();
                throw;
            }
        }
    }
}

DynamoDB Transactions

TransactWrite

const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, TransactWriteCommand } = require('@aws-sdk/lib-dynamodb');

const client = new DynamoDBClient({ region: 'us-east-1' });
const docClient = DynamoDBDocumentClient.from(client);

async function transferBalance(fromUserId, toUserId, amount) {
  const command = new TransactWriteCommand({
    TransactItems: [
      {
        Update: {
          TableName: 'Users',
          Key: { userId: fromUserId },
          UpdateExpression: 'SET balance = balance - :amount',
          ConditionExpression: 'balance >= :amount',
          ExpressionAttributeValues: {
            ':amount': amount
          }
        }
      },
      {
        Update: {
          TableName: 'Users',
          Key: { userId: toUserId },
          UpdateExpression: 'SET balance = balance + :amount',
          ExpressionAttributeValues: {
            ':amount': amount
          }
        }
      },
      {
        Put: {
          TableName: 'Transactions',
          Item: {
            transactionId: generateId(),
            from: fromUserId,
            to: toUserId,
            amount,
            timestamp: new Date().toISOString()
          }
        }
      }
    ]
  });
  
  try {
    await docClient.send(command);
    console.log('Transaction successful');
  } catch (error) {
    if (error.name === 'TransactionCanceledException') {
      console.error('Transaction failed:', error.CancellationReasons);
    }
    throw error;
  }
}

TransactGet

const { TransactGetCommand } = require('@aws-sdk/lib-dynamodb');

async function getMultipleItems() {
  const command = new TransactGetCommand({
    TransactItems: [
      {
        Get: {
          TableName: 'Users',
          Key: { userId: 'user1' }
        }
      },
      {
        Get: {
          TableName: 'Orders',
          Key: { orderId: 'order1' }
        }
      }
    ]
  });
  
  const response = await docClient.send(command);
  return response.Responses.map(r => r.Item);
}

Cassandra Lightweight Transactions

Compare-and-Set

const cassandra = require('cassandra-driver');

const client = new cassandra.Client({
  contactPoints: ['localhost'],
  localDataCenter: 'datacenter1',
  keyspace: 'myapp'
});

// Lightweight transaction (LWT)
const result = await client.execute(
  'UPDATE accounts SET balance = balance - ? WHERE id = ? IF balance >= ?',
  [100, 'account1', 100],
  { prepare: true }
);

if (result.rows[0]['[applied]']) {
  console.log('Transaction applied');
} else {
  console.log('Condition not met');
}

// Insert if not exists
await client.execute(
  'INSERT INTO users (id, name, email) VALUES (?, ?, ?) IF NOT EXISTS',
  [userId, name, email],
  { prepare: true }
);

Batched Statements

// Logged batch (atomic)
const queries = [
  {
    query: 'UPDATE accounts SET balance = balance - ? WHERE id = ?',
    params: [100, 'account1']
  },
  {
    query: 'UPDATE accounts SET balance = balance + ? WHERE id = ?',
    params: [100, 'account2']
  }
];

await client.batch(queries, { prepare: true, logged: true });

// Unlogged batch (not atomic, better performance)
await client.batch(queries, { prepare: true, logged: false });

Redis Transactions

MULTI/EXEC

const redis = require('redis');
const client = redis.createClient();
await client.connect();

// Transaction
const multi = client.multi();

multi.decrBy('account:1:balance', 100);
multi.incrBy('account:2:balance', 100);
multi.hSet('transaction:123', {
  from: 'account:1',
  to: 'account:2',
  amount: '100'
});

const results = await multi.exec();
console.log('Transaction results:', results);

Optimistic Locking with WATCH

async function transferWithWatch(fromKey, toKey, amount) {
  while (true) {
    await client.watch(fromKey);
    
    const balance = parseInt(await client.get(fromKey));
    
    if (balance < amount) {
      await client.unwatch();
      throw new Error('Insufficient balance');
    }
    
    const multi = client.multi();
    multi.decrBy(fromKey, amount);
    multi.incrBy(toKey, amount);
    
    try {
      await multi.exec();
      break;  // Success
    } catch (error) {
      // WATCH failed, retry
      continue;
    }
  }
}

Lua Scripts (Atomic)

// Lua script for atomic operations
const script = `
  local balance = redis.call('GET', KEYS[1])
  if tonumber(balance) >= tonumber(ARGV[1]) then
    redis.call('DECRBY', KEYS[1], ARGV[1])
    redis.call('INCRBY', KEYS[2], ARGV[1])
    return 1
  else
    return 0
  end
`;

const result = await client.eval(script, {
  keys: ['account:1:balance', 'account:2:balance'],
  arguments: ['100']
});

if (result === 1) {
  console.log('Transfer successful');
} else {
  console.log('Insufficient balance');
}

Compensating Transactions (Saga Pattern)

// For databases without full ACID support
class SagaOrchestrator {
  async executeOrder(orderData) {
    const compensations = [];
    
    try {
      // Step 1: Reserve inventory
      await this.reserveInventory(orderData.items);
      compensations.push(() => this.releaseInventory(orderData.items));
      
      // Step 2: Process payment
      await this.processPayment(orderData.payment);
      compensations.push(() => this.refundPayment(orderData.payment));
      
      // Step 3: Create order
      const order = await this.createOrder(orderData);
      
      return order;
    } catch (error) {
      // Rollback in reverse order
      for (const compensate of compensations.reverse()) {
        await compensate();
      }
      throw error;
    }
  }
  
  async reserveInventory(items) {
    // Reserve inventory in inventory service
  }
  
  async releaseInventory(items) {
    // Compensating action
  }
  
  async processPayment(payment) {
    // Process payment in payment service
  }
  
  async refundPayment(payment) {
    // Compensating action
  }
}

Two-Phase Commit Pattern

// Manual 2PC for distributed transactions
class TwoPhaseCommit {
  async transfer(fromAccount, toAccount, amount) {
    const transactionId = generateId();
    
    // Phase 1: Prepare
    const prepared = await Promise.all([
      this.prepare(fromAccount, -amount, transactionId),
      this.prepare(toAccount, amount, transactionId)
    ]);
    
    if (!prepared.every(p => p)) {
      // Abort
      await this.abort(transactionId);
      throw new Error('Transaction aborted');
    }
    
    // Phase 2: Commit
    await Promise.all([
      this.commit(fromAccount, transactionId),
      this.commit(toAccount, transactionId)
    ]);
  }
  
  async prepare(account, amount, txId) {
    // Mark as prepared in database
    return await db.collection('accounts').updateOne(
      { _id: account, 'transactions.id': { $ne: txId } },
      {
        $push: {
          transactions: {
            id: txId,
            amount,
            state: 'prepared'
          }
        }
      }
    );
  }
  
  async commit(account, txId) {
    // Apply transaction
    const doc = await db.collection('accounts').findOne({ _id: account });
    const tx = doc.transactions.find(t => t.id === txId);
    
    await db.collection('accounts').updateOne(
      { _id: account },
      {
        $inc: { balance: tx.amount },
        $pull: { transactions: { id: txId } }
      }
    );
  }
}

Transaction Isolation Levels

const isolationLevels = {
  mongodb: {
    snapshot: 'Read from consistent snapshot',
    local: 'Read latest committed data',
    majority: 'Read data acknowledged by majority'
  },
  
  cassandra: {
    serial: 'Linearizable consistency (LWT)',
    localSerial: 'Linearizable in local DC'
  },
  
  dynamodb: {
    serializable: 'All transactions serializable'
  }
};

Best Practices

const transactionBestPractices = [
  'Keep transactions short',
  'Minimize operations in transaction',
  'Handle transaction failures with retries',
  'Use appropriate isolation level',
  'Avoid long-running transactions',
  'Consider eventual consistency alternatives',
  'Use compensating transactions for sagas',
  'Monitor transaction performance'
];

Interview Tips

  • Explain ACID: Atomicity, Consistency, Isolation, Durability
  • Show MongoDB: Multi-document transactions
  • Demonstrate DynamoDB: TransactWrite, TransactGet
  • Discuss Cassandra: Lightweight transactions, batches
  • Mention Redis: MULTI/EXEC, Lua scripts
  • Show patterns: Saga, two-phase commit

Summary

NoSQL databases have varying transaction support. MongoDB offers multi-document ACID transactions with sessions. DynamoDB provides TransactWrite/TransactGet for up to 25 items. Cassandra supports lightweight transactions (LWT) and batched statements. Redis uses MULTI/EXEC and Lua scripts for atomicity. For distributed transactions, use saga pattern with compensating transactions. Keep transactions short and handle failures. Essential for maintaining data consistency in NoSQL systems.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Test Your Nosql Knowledge

Ready to put your skills to the test? Take our interactive Nosql quiz and get instant feedback on your answers.