WeCodeAcademy

Step 4: Transfers are all-or-nothing

A transfer is two changes: take money out of one account, put it into another. If the second step fails after the first succeeded, money has vanished. The code below handles that: it withdraws, then deposits, and if the deposit throws, it rolls back the withdrawal and re-throws. It also adds a fee for large transfers and refuses transfers to the same account. Look at which requests succeed, and check that the total money never changes except for the fee.

ExampleJava
import java.util.*;

public class Main {
  static class InsufficientFundsException extends RuntimeException {
    InsufficientFundsException(String m) { super(m); }
  }

  static class Account {
    final String name;
    long cents;
    Account(String name, long cents) { this.name = name; this.cents = cents; }
    void withdraw(long amount) {
      if (amount > cents) throw new InsufficientFundsException(name + " cannot pay " + amount);
      cents -= amount;
    }
    void deposit(long amount) { cents += amount; }
    public String toString() { return name + "=" + cents; }
  }

  // All or nothing: if the deposit side fails, the withdrawal is undone.
  static void transfer(Account from, Account to, long amount) {
    if (from == to) throw new IllegalArgumentException("cannot transfer to the same account");
    long fee = amount >= 10000 ? 100 : 0;                  // 1.00 fee for large transfers
    from.withdraw(amount + fee);
    try {
      if (to.name.equals("frozen")) throw new IllegalStateException("account frozen");
      to.deposit(amount);
    } catch (RuntimeException e) {
      from.deposit(amount + fee);                          // roll back
      throw e;
    }
  }

  public static void main(String[] args) {
    Account ada = new Account("Ada", 50000);
    Account sam = new Account("Sam", 1000);
    Account frozen = new Account("frozen", 0);

    Object[][] requests = { { ada, sam, 2000L }, { sam, ada, 99999L }, { ada, frozen, 500L }, { ada, sam, 10000L }, { ada, ada, 1L } };
    for (Object[] r : requests) {
      Account from = (Account) r[0], to = (Account) r[1];
      long amount = (Long) r[2];
      try {
        transfer(from, to, amount);
        System.out.println("OK    " + from.name + " -> " + to.name + " " + amount);
      } catch (RuntimeException e) {
        System.out.println("ERROR " + from.name + " -> " + to.name + " " + amount + ": " + e.getMessage());
      }
    }
    System.out.println(ada + " " + sam + " " + frozen);
    System.out.println("total " + (ada.cents + sam.cents + frozen.cents));
  }
}
Output
OK    Ada -> Sam 2000
ERROR Sam -> Ada 99999: Sam cannot pay 100099
ERROR Ada -> frozen 500: account frozen
OK    Ada -> Sam 10000
ERROR Ada -> Ada 1: cannot transfer to the same account
Ada=37900 Sam=13000 frozen=0
total 50900

Three of the five requests were refused, and the refused ones changed nothing: the frozen account's failed deposit was rolled back, so Ada did not lose 500. The total is 50900, not the starting 51000: the 100 fee on the 10000 transfer was withdrawn from Ada and deposited nowhere. In a real bank the fee would be deposited into a bank-owned account so the books always balance. Adding that account is a good exercise.

Quick check

What should happen to the first withdrawal if the deposit then fails?

Finish the quick check (and any code exercise) to complete this lesson, or mark it done yourself.

Tip: use the left and right arrow keys to move between lessons.