Back to Synchronization
Q5Question 5 of 8

How do you use synchronized for bank account access?

Key Concepts
  • Use per-account locks so operations on different accounts run in parallel.
  • Use synchronized blocks (not methods) to minimize lock hold time.
  • The balance check and debit must be atomic: both inside the same synchronized block.
  • For transfers between two accounts, acquire locks in consistent order (by account ID) to prevent deadlock.
  • Never use a global lock for all accounts: it makes every operation sequential.
  • In production, combine in-memory locks with database row-level locks (SELECT FOR UPDATE) for distributed safety.

Definition

Use per-account locks with synchronized blocks to protect concurrent balance operations, and acquire multiple account locks in consistent order to prevent deadlock during transfers.
  • Per-account lock – each Account has its own private final Object lock. Operations on different accounts run in parallel (different locks). Operations on the same account are serialized (same lock).
  • Synchronized block over method – lock only the balance check and modification, not the surrounding validation, logging, and notification.
  • Atomic check-then-act – the balance check (balance >= amount) and the debit (balance -= amount) must be inside the same synchronized block. Otherwise, another thread can change the balance between the check and the debit.
  • Consistent lock ordering – when a transfer locks two accounts, always lock them in a deterministic order (by account ID) to prevent circular wait deadlock.

Rajesh and Priya both try to withdraw from Rajesh's account simultaneously via ATM and mobile banking. Without synchronization, both read the same balance of Rs.100,000, both see enough for Rs.60,000, and both succeed, leaving the balance at Rs.40,000 instead of rejecting one. With per-account locking, the ATM transaction locks Rajesh's account, checks the balance, debits, and unlocks. The mobile transaction then locks the account, sees Rs.40,000, and rejects the Rs.60,000 withdrawal.

Understanding the Concepts

How do you implement per-account locking?

Each Account has its own lock object. Operations on different accounts proceed in parallel.

JAVA
package com.bank.model;

import java.math.BigDecimal;

public class Account {
    private final String accountId;
    private final Object lock = new Object();
    private BigDecimal balance;

    public Account(String accountId, BigDecimal initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }

    public void debit(BigDecimal amount) {
        synchronized (lock) {
            if (balance.compareTo(amount) < 0) {
                throw new IllegalStateException(
                    "Insufficient funds in " + accountId
                    + ": balance=" + balance + ", requested=" + amount);
            }
            balance = balance.subtract(amount);
        }
    }

    public void credit(BigDecimal amount) {
        synchronized (lock) {
            balance = balance.add(amount);
        }
    }

    public BigDecimal getBalance() {
        synchronized (lock) {
            return balance;
        }
    }

    public String getAccountId() { return accountId; }
    public Object getLock() { return lock; }
}

Rajesh's debit and Priya's debit use different lock objects, so they run in parallel. Two debits on Rajesh's account use the same lock, so they are serialized.

How do you implement a safe transfer between two accounts?

A transfer must lock both accounts. To prevent deadlock, always lock in consistent order by account ID.

JAVA
package com.bank.service;

import com.bank.model.Account;
import java.math.BigDecimal;

public class TransferService {
    public void transfer(Account from, Account to, BigDecimal amount) {
        // Always lock in consistent order by account ID
        Account first = from.getAccountId().compareTo(to.getAccountId()) < 0
            ? from : to;
        Account second = (first == from) ? to : from;

        synchronized (first.getLock()) {
            synchronized (second.getLock()) {
                if (from.getBalance().compareTo(amount) < 0) {
                    throw new IllegalStateException("Insufficient funds");
                }
                from.debit(amount);
                to.credit(amount);
            }
        }
    }
}

Why is lock ordering essential for transfers?

Without ordering, two simultaneous transfers can deadlock.

TEXT
Without ordering (DEADLOCK):
  Thread-1: transfer(Rajesh -> Priya)
    locks Rajesh (SAV2001)
    waits for Priya (CUR3001)
  Thread-2: transfer(Priya -> Rajesh)
    locks Priya (CUR3001)
    waits for Rajesh (SAV2001)
  Both wait forever!

With ordering (SAFE):
  Thread-1: transfer(Rajesh -> Priya)
    locks CUR3001 (Priya first, lower ID)
    locks SAV2001 (Rajesh second)
  Thread-2: transfer(Priya -> Rajesh)
    waits for CUR3001 (same order!)
    Thread-1 finishes, releases both locks
    Thread-2 proceeds

Why should you avoid a global lock?

A single lock for all accounts makes every operation sequential, regardless of which account is being accessed.

JAVA
package com.bank.service;

public class GlobalLockAntiPattern {
    // WRONG: one lock for ALL 10,000 accounts
    private static final Object GLOBAL_LOCK = new Object();

    public void debit(Account account, java.math.BigDecimal amount) {
        synchronized (GLOBAL_LOCK) {
            account.debit(amount);
        }
    }
    // 10,000 accounts, 1 lock: only 1 operation at a time across ALL accounts!
    // Rajesh's debit blocks Priya's credit even though they are different accounts.
}

With per-account locks, 10,000 accounts can have 10,000 concurrent operations.

What about database-level locking?

In a distributed banking system with multiple JVM instances, in-memory locks are not sufficient. Use database row-level locks.

SQL
-- Lock the specific account row for the duration of the transaction
BEGIN;
SELECT balance FROM accounts WHERE account_id = 'SAV2001' FOR UPDATE;
-- Only this transaction can modify SAV2001 until COMMIT
UPDATE accounts SET balance = balance - 50000 WHERE account_id = 'SAV2001';
COMMIT;

In production, combine both: in-memory locks for single-JVM consistency and database locks for cross-JVM consistency.

What should a senior engineer know?

  • Lock striping: ConcurrentHashMap uses lock striping internally (16 segments by default). You can apply the same pattern: hash the account ID to one of N locks, allowing N concurrent operations. This is a middle ground between global lock (1 lock) and per-account lock (N locks).
  • Optimistic locking: instead of locking, read the balance with a version number. When writing, check that the version has not changed. If it has, retry. This is how JPA @Version works.
  • Distributed locks: for multi-JVM environments, use Redis-based locks (Redisson) or database advisory locks.

Visual Overview

DIAGRAM
  Per-Account Locking:

  Account SAV2001 (Rajesh)        Account CUR3001 (Priya)
  ┌───────────────────────┐       ┌───────────────────────┐
  │ lock: Object@a1        │       │ lock: Object@b2        │
  │ balance: 100,000       │       │ balance: 50,000        │
  └───────────────────────┘       └───────────────────────┘
        │                               │
        ▼                               ▼
  Thread-1: debit(50K)            Thread-2: credit(20K)
  synchronized(lock@a1)           synchronized(lock@b2)
  DIFFERENT locks ─── run in PARALLEL

  Transfer: Rajesh -> Priya (consistent order by ID)
  Step 1: lock CUR3001 (Priya, lower ID)
  Step 2: lock SAV2001 (Rajesh, higher ID)
  Step 3: debit Rajesh, credit Priya
  Step 4: unlock SAV2001, unlock CUR3001

Key Takeaway

In a banking application with concurrent account access, use per-account locks so that operations on different accounts run in parallel. Use synchronized blocks (not methods) to lock only the balance check and modification, keeping lock hold time minimal. The balance check and the debit must be inside the same synchronized block to prevent the check-then-act race condition.

For transfers between two accounts, always acquire locks in consistent order by account ID. Without ordering, two simultaneous transfers in opposite directions deadlock. With ordering, both threads lock the lower ID first, and one thread waits for the other to finish.

Remember one sentence: use per-account locks for parallelism, synchronized blocks for minimal lock time, and consistent lock ordering for deadlock-free transfers.

---

Share
Use ← → arrow keys to navigate