I am trying to solve the much talked about problem of transferring money from one account to another in a thread safe manner, given that the accounts exist in memory only.
I was able to easily solve the problem with eventual balance consistency
using CAS. However, looking through other solutions on the internet everyone seems to be adamant on using explicit locks to allow for atomicity of transaction but in this case even though transaction operations i.e. withdraw and then deposit are not atomic but are eventually consistent.
I am trying to understand why shall I use a lock? The eventual state is always consistent and there's no possibility of negative balances
This is my simple implementation
public class Account {
private final long id;
private final AtomicReference<BigDecimal> balance;
public Account(long id, BigDecimal balance) {
this.id = id;
this.balance = new AtomicReference<>(balance);
}
public long getId() {
return id;
}
public void withdraw(BigDecimal amount) {
BigDecimal currentBalance, newBalance;
do {
currentBalance = balance.get();
if(currentBalancepareTo(amount) < 0) {
throw new InsufficientBalanceException("Unable to withdraw amount: " + amount +
" which is greater than existing balance: " + balance);
}
newBalance = currentBalance.subtract(amount);
} while (!balancepareAndSet(currentBalance, newBalance));
}
public void deposit(BigDecimal amount) {
BigDecimal currentBalance, newBalance;
do {
currentBalance = balance.get();
newBalance = currentBalance.add(amount);
} while (!balancepareAndSet(currentBalance, newBalance));
}
private static class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String msg) {
super(msg);
}
}
}
and then for Accounts transfer I have a service class,
public class AccountServiceAllowVariance {
public boolean transfer(Account source, Account destination, BigDecimal amount) {
Objects.requireNonNull(source, "``from` account cannot be null");
Objects.requireNonNull(destination, "`to` account cannot be null");
Objects.requireNonNull(amount, "`amount` cannot be null");
if(source.equals(destination)) {
throw new IllegalArgumentException("`from` & `to` accounts cannot be same");
}
if(amountpareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("`amount` must be greater than 0");
}
source.withdraw(amount);
destination.deposit(amount);
return true;
}
}
Now, I am trying to understand in what case could the consistency be lost? I believe this is the most high performant we can become.