Machine Coding Problem

ATM Machine

maco30maco60macoAllfintechchain-of-responsibility-&-state-pattern
Commonly Asked By:GoogleMicrosoftJPMorgan Chase

Requirements & System Scope

Functional Scope (In-Scope)

  • State-Driven Terminal Control: Support distinct step flows (Idle, CardInserted, PINVerified, Dispensing) using structured State Pattern classes.
  • Greedy Cash Dispensation: Distribute banknotes dynamically matching high-to-low greedy note alignments using a Chain of Responsibility pattern.
  • Atomic Account Debiting: Prevent partial payout errors by locking cash and debiting accounts atomically.
  • Fail-Safe Banknote Rollbacks: On failure to dispense requested note patterns, roll back balance subtractions instantly.

Explicit Boundaries (Out-of-Scope)

  • No Physical Mechanical Hardware Control: Rollout ignores thermal slip printer APIs, magnetic strip reads, or physical deposit vaults.
  • No Central Network Routing Broker: Assumes direct secure loop connections to bank accounts.

Class Diagram & Entity Relationships

Structural layout showing clean separation of active states and banknote registers:

Loading...
  • Polymorphic Context state: ATMState tracks step sequences, shielding core account debit operations from direct out-of-order execution attempts.
  • Decoupled note Dispenser: CashDispenser houses banknote configurations isolated from bank accounts.

Design Patterns & SOLID Principles

  • State Design Pattern: Encapsulating state rules protects internal logic from illegal API triggers by delegating state actions to concrete classes.
  • Chain of Responsibility Pattern: ATM banknote dispensing uses note denomination links ($100 -> $50 -> $20) to recursively compute banknote distribution.
  • Single Responsibility Principle (SRP):CashDispenser manages physical money notes inventory. Account encapsulates user funds. ATMMachine handles overall user sessions.

Core Execution Workflows

Withdrawal Verification Lifecycle

  1. Client inserts card, transitioning context to CARD_INSERTED.
  2. User submits PIN, transitioning state to PIN_VERIFIED upon validation match.
  3. User invokes cash request: withdraw(amount):
    1. Acquire exclusive account lock.
    2. Verify balance: balance >= amount.
    3. Deduct funds locally (debit phase 1).
    4. Acquire exclusive CashDispenser inventory lock.
    5. Check note combinations using Chain of Responsibility. If match fails, restore balance immediately (debit rollback) and return.
    6. If notes are dispensed successfully, eject card and reset back to IDLE.

Concurrency & Thread Safety Strategy

The critical concurrency vulnerability occurs when multiple online requests (e.g. online transfers and local ATM withdrawals) attempt to modify the same account simultaneously:

  • Strict Synchronized Account Mutexes: Wrap balance adjustments inside localized synchronized locks to isolate multi-thread writes.
  • Dispenser Mutex Isolation: Note inventory updates run under dedicated monitors, preventing thread-level double allocation.

Complete Clean Code Blueprint

Practical reference designs showing ATM state control and transaction safety in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

interface ATMState {
    void insertCard(ATMMachine atm, String accountNumber);
    void enterPin(ATMMachine atm, String pin);
    void withdraw(ATMMachine atm, int amount);
    void cancel(ATMMachine atm);
}

class IdleState implements ATMState {
    @Override
    public void insertCard(ATMMachine atm, String accountNumber) {
        Account acc = atm.getAccount(accountNumber);
        if (acc != null) {
            atm.setActiveAccount(acc);
            atm.setState(new CardInsertedState());
            System.out.println("[ATM State] Card inserted successfully for: " + accountNumber);
        } else {
            System.out.println("[ATM State] Invalid Card: Account not found.");
        }
    }
    @Override public void enterPin(ATMMachine atm, String pin) { System.out.println("[ATM ERROR] Please insert card first."); }
    @Override public void withdraw(ATMMachine atm, int amount) { System.out.println("[ATM ERROR] Please insert card first."); }
    @Override public void cancel(ATMMachine atm) { System.out.println("[ATM ERROR] No active session to cancel."); }
}

class CardInsertedState implements ATMState {
    @Override public void insertCard(ATMMachine atm, String accountNumber) { System.out.println("[ATM ERROR] Card already inside."); }
    @Override
    public void enterPin(ATMMachine atm, String pin) {
        if (atm.getActiveAccount().verifyPin(pin)) {
            atm.setState(new PinVerifiedState());
            System.out.println("[ATM State] PIN Verified. Welcome back!");
        } else {
            System.out.println("[ATM State] Incorrect PIN. Session terminated.");
            atm.cancel();
        }
    }
    @Override public void withdraw(ATMMachine atm, int amount) { System.out.println("[ATM ERROR] Please enter PIN first."); }
    @Override
    public void cancel(ATMMachine atm) {
        System.out.println("[ATM State] Session cancelled. Card returned.");
        atm.setActiveAccount(null);
        atm.setState(new IdleState());
    }
}

class PinVerifiedState implements ATMState {
    @Override public void insertCard(ATMMachine atm, String accountNumber) { System.out.println("[ATM ERROR] Card already inside."); }
    @Override public void enterPin(ATMMachine atm, String pin) { System.out.println("[ATM ERROR] PIN already verified."); }
    @Override
    public void withdraw(ATMMachine atm, int amount) {
        atm.setState(new DispensingState());
        System.out.println("[ATM State] Processing withdrawal of $" + amount + "...");
        atm.dispenseCash(amount);
    }
    @Override
    public void cancel(ATMMachine atm) {
        System.out.println("[ATM State] Ejecting card. Session completed.");
        atm.setActiveAccount(null);
        atm.setState(new IdleState());
    }
}

class DispensingState implements ATMState {
    @Override public void insertCard(ATMMachine atm, String accNum) { System.out.println("[ATM ERROR] Please wait, currently dispensing."); }
    @Override public void enterPin(ATMMachine atm, String pin) { System.out.println("[ATM ERROR] Please wait, currently dispensing."); }
    @Override public void withdraw(ATMMachine atm, int amount) { System.out.println("[ATM ERROR] Please wait, currently dispensing."); }
    @Override public void cancel(ATMMachine atm) { System.out.println("[ATM ERROR] Cannot cancel while dispensing cash."); }
}

interface CashDispenseChain {
    void setNext(CashDispenseChain next);
    boolean dispense(int amount, Map<Integer, Integer> dispensedNotes, Map<Integer, Integer> inventory);
}

class NoteDispenser implements CashDispenseChain {
    private final int denomination;
    private CashDispenseChain next;

    public NoteDispenser(int denomination) {
        this.denomination = denomination;
    }

    @Override
    public void setNext(CashDispenseChain next) {
        this.next = next;
    }

    @Override
    public boolean dispense(int amount, Map<Integer, Integer> dispensedNotes, Map<Integer, Integer> inventory) {
        if (amount <= 0) return true;
        int available = inventory.getOrDefault(denomination, 0);
        int needed = amount / denomination;
        int actual = Math.min(needed, available);

        if (actual > 0) {
            dispensedNotes.put(denomination, actual);
            amount -= actual * denomination;
        }

        if (amount > 0) {
            if (next != null) {
                return next.dispense(amount, dispensedNotes, inventory);
            }
            return false;
        }
        return true;
    }
}

class CashDispenser {
    private final Map<Integer, Integer> noteInventory = new ConcurrentHashMap<>();
    private final CashDispenseChain chain;
    private final ReentrantLock lock = new ReentrantLock();

    public CashDispenser() {
        noteInventory.put(100, 20); // $2000
        noteInventory.put(50, 40);  // $2000
        noteInventory.put(20, 100); // $2000

        CashDispenseChain c100 = new NoteDispenser(100);
        CashDispenseChain c50 = new NoteDispenser(50);
        CashDispenseChain c20 = new NoteDispenser(20);

        c100.setNext(c50);
        c50.setNext(c20);
        this.chain = c100;
    }

    public boolean dispense(int amount) {
        lock.lock();
        try {
            Map<Integer, Integer> dispensed = new HashMap<>();
            if (chain.dispense(amount, dispensed, noteInventory)) {
                for (Map.Entry<Integer, Integer> entry : dispensed.entrySet()) {
                    noteInventory.put(entry.getKey(), noteInventory.get(entry.getKey()) - entry.getValue());
                }
                System.out.println("[Dispenser] Dispensed bills: " + dispensed);
                return true;
            }
            System.out.println("[Dispenser] Cannot dispense requested amount with current bill combinations!");
            return false;
        } finally {
            lock.unlock();
        }
    }
}

class Account {
    private final String accountNumber;
    private final String pin;
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();

    public Account(String accountNumber, String pin, double initialBalance) {
        this.accountNumber = accountNumber;
        this.pin = pin;
        this.balance = initialBalance;
    }

    public String getAccountNumber() { return accountNumber; }
    public boolean verifyPin(String enteredPin) { return pin.equals(enteredPin); }
    
    public double getBalance() {
        lock.lock();
        try { return balance; } finally { lock.unlock(); }
    }

    public boolean debit(double amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public void credit(double amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();
        }
    }
}

class ATMMachine {
    private ATMState state = new IdleState();
    private Account activeAccount = null;
    private final CashDispenser dispenser = new CashDispenser();
    private final Map<String, Account> accounts = new ConcurrentHashMap<>();
    private final ReentrantLock atmLock = new ReentrantLock();

    public void registerAccount(Account acc) {
        accounts.put(acc.getAccountNumber(), acc);
    }

    public Account getAccount(String accountNumber) {
        return accounts.get(accountNumber);
    }

    public void setState(ATMState state) {
        this.state = state;
    }

    public void setActiveAccount(Account acc) {
        this.activeAccount = acc;
    }

    public Account getActiveAccount() {
        return activeAccount;
    }

    public void insertCard(String accountNumber) {
        atmLock.lock();
        try { state.insertCard(this, accountNumber); } finally { atmLock.unlock(); }
    }

    public void enterPin(String pin) {
        atmLock.lock();
        try { state.enterPin(this, pin); } finally { atmLock.unlock(); }
    }

    public void withdraw(int amount) {
        atmLock.lock();
        try { state.withdraw(this, amount); } finally { atmLock.unlock(); }
    }

    public void cancel() {
        atmLock.lock();
        try { state.cancel(this); } finally { atmLock.unlock(); }
    }

    public void dispenseCash(int amount) {
        if (activeAccount == null) return;
        if (activeAccount.debit(amount)) {
            if (dispenser.dispense(amount)) {
                System.out.println("[ATM Success] Completed withdraw of $" + amount + ". Ejecting card.");
                state = new IdleState();
                activeAccount = null;
            } else {
                System.out.println("[ATM Error] Dispensation error. Restoring account balance.");
                activeAccount.credit(amount);
                state = new PinVerifiedState();
            }
        } else {
            System.out.println("[ATM Error] Insufficient account balance.");
            state = new PinVerifiedState();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== ATM CONCURRENT SIMULATION DRIVER ===");
        ATMMachine atm = new ATMMachine();
        Account acc = new Account("1234-5678", "4321", 500.00);
        atm.registerAccount(acc);

        System.out.println("Initial Balance: $" + acc.getBalance());
        
        atm.insertCard("1234-5678");
        atm.enterPin("4321");
        atm.withdraw(280); // Should dispense 2x$100, 1x$50, 1x$20 = $270 + $10 fails if denomination is 20/50/100 only! Wait, 280: 2x100 + 1x50 + 1x20 = 270 (10 left - fail)
        
        // Let's retry with a multiple of 20/50/100
        atm.withdraw(290); // 2x100, 1x50, 2x20 = 290! Dispenses successfully.
        
        System.out.println("Final Account Balance: $" + acc.getBalance());
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...