Skip to main content

JS - Concurrency

· 3 min read

Imagine you have a bank account and 10 transactions arrive at the same time — some deposit money, some withdraw it.

How do you ensure that all transactions are processed correctly without overwriting each other?


let accountBalance = 100;

// Simulating Concurrent Transactions
async function runTransactions() {
console.log(`Starting Balance: ${accountBalance}`);

await Promise.all([
addAmount(50), // Should be 150
addAmount(-30), // Should be 120
addAmount(20), // Should be 140
addAmount(-10), // Should be 130
addAmount(40) // Should be 170 --> Only this returns final output
]);

console.log(`Final Balance: ${accountBalance}`);
// Expected: 170, Actual: 140 as all transactions start with 100 balance
}

async function addAmount(amount) {
const balance = await getBalance();

const newBalance = balance + amount;

console.log(
"Read Balance:", balance,
"| Adding:", amount,
"| New Balance:", newBalance
);

await saveBalance(newBalance);
}

async function getBalance() {
return accountBalance;
}

async function saveBalance(newBalance){
accountBalance = newBalance;
}
runTransactions();
Read Balance: 100 | Adding: 50 | New Balance: 150
Read Balance: 100 | Adding: -30 | New Balance: 70
Read Balance: 100 | Adding: 20 | New Balance: 120
Read Balance: 100 | Adding: -10 | New Balance: 90
Read Balance: 100 | Adding: 40 | New Balance: 140

Final Balance: 140
  1. Each transaction reads 100 instead of the updated balance.
  2. Transactions compute their own new balance separately:
  3. Last transaction (+40) overwrites previous updates.

Challenges

How do we prevent multiple operations from interfering with each other?

  • Locks? JavaScript doesn’t have built-in locks.
  • Sequential execution? But we still want concurrency.
  • Atomic updates? Possible, but how?

Solution: Amotic Updates using MUTEX

A Mutex (Mutual Exclusion) ensures that only one operation modifies the balance at a time.

function createMutex() {
let locked = false;
const queue = [];

return {
lock: function() {
// Each lock call returns a promise
// the caller of lock, awaits for this to resolve
return new Promise((resolve) => {

if(!locked) {
// No one holds the lock yet - take it

locked = true;
resolve();
} else {
// lock is taken - queue up the resolver

queue.push(resolve);
}
});
},

unlock: function() {

if(queue.length > 0) {
// hand off the lock to the next waiting caller

const nextResolve = queue.shift();
nextResolve();
} else {
// no one is waiting - release the lock

locked = false;
}
}
}
}


const mutex = createMutex();

// ✅ Mutex-Protected Function
async function addAmountConcurrent(amount) {

await mutex.lock();

const balance = await getBalance();

const newBalance = balance + amount;
console.log(
"💰 Read Balance:", balance,
"| Adding:", amount,
"| New Balance:", newBalance
);

await saveBalance(newBalance)

mutex.unlock();
}

let accountBalance = 100;

async function getBalance() {
return accountBalance;
}

async function saveBalance(newBalance) {
accountBalance = newBalance;
}

async function runTransactionsMutex() {
console.log(`Starting Balance: ${accountBalance}`);

await Promise.all([
addAmountConcurrent(50),
addAmountConcurrent(-30),
addAmountConcurrent(20),
addAmountConcurrent(-10),
addAmountConcurrent(40)
]);

console.log(`💰 Final Balance: ${accountBalance}`);
}

Fixed final output

runTransactionsMutex();
💰 Read Balance: 100 | Adding: 50 | New Balance: 150
💰 Read Balance: 150 | Adding: -30 | New Balance: 120
💰 Read Balance: 120 | Adding: 20 | New Balance: 140
💰 Read Balance: 140 | Adding: -10 | New Balance: 130
💰 Read Balance: 130 | Adding: 40 | New Balance: 170

💰 Final Balance: 170