PythonStudio
Interview Problems

ATM

medium · transactions, fault tolerance, rollback

Brief-reported interview attribution (unverified): Amazon, Microsoft

How to approach an LLD round

Design an ATM. It holds accounts (account_id → balance in cents and a PIN) and a cash inventory by denomination (100, 50, 20, 10). The core rule: **a withdrawal is a transaction — the account is debited only if the cash is actually dispensed. If dispensing fails, the debit is rolled back.** For testability the ATM has a fault-injection method fail_next_dispense() that makes the next dispense fail (in production this is the hardware error).

class ATM:
    def __init__(self, accounts, cash):
        # accounts: {account_id: {"balance": int, "pin": str}}; cash: {denom: count}
    def balance(self, account_id, pin):
        # -> balance, or None for a wrong PIN / unknown account
    def deposit(self, account_id, amount):
        # -> new balance, or None for an unknown account / amount <= 0
        # (deposited cash is added to the inventory, largest denominations first)
    def withdraw(self, account_id, pin, amount):
        # -> ("ok", {denom: count}) or ("error", reason)
        # reasons: "wrong pin", "invalid amount", "insufficient funds",
        #          "cannot dispense", "dispense failed"
    def fail_next_dispense(self):
        # test hook: the next withdraw attempt fails at the dispense step
    def cash_on_hand(self):
        # -> total cash in the machine

Rules. Withdrawal checks, in order: PIN, amount > 0 and a multiple of 10, balance ≥ amount, the amount can be formed from the denominations, and the inventory actually holds those notes (greedy, largest first). Only then is the account debited; if the dispense fails, the debit is rolled back and the error is "dispense failed". deposit adds the amount to the balance and to the cash inventory (greedy decomposition into denominations).

Money units: account balances, deposits, withdrawals and denominations are integer cents. Denomination 100 means 100 cents.

Tests use explicit inputs and injected clocks/callbacks. No network, real threads or application-server execution is involved.

Public demo

# Public demo: run after implementing the class API above.
a = ATM({'A1': {'balance': 500, 'pin': '1234'}}, {100: 5, 50: 5, 20: 5, 10: 5})
print(a.balance('A1', '1234'))
print(a.balance('A1', '0000'))
print(a.balance('ZZ', '1234'))

Starter code

class ATM:

    def __init__(self, accounts, cash):
        """accounts: {account_id: {"balance": int, "pin": str}}; cash: {denom: count}"""
        pass

    def fail_next_dispense(self):
        """test hook: the next withdraw attempt fails at the dispense step"""
        pass

    def balance(self, account_id, pin):
        """-> balance, or None for a wrong PIN / unknown account"""
        pass

    def deposit(self, account_id, amount):
        """-> new balance, or None for an unknown account / amount <= 0 (deposited cash is added to the inventory, largest denominations first)"""
        pass

    def withdraw(self, account_id, pin, amount):
        '''-> ("ok", {denom: count}) or ("error", reason) reasons: "wrong pin", "invalid amount", "insufficient funds", "cannot dispense", "dispense failed"'''
        pass

    def cash_on_hand(self):
        """-> total cash in the machine"""
        pass
Loading editor…

Each test has a five-second execution limit. Your practice drafts and solved progress stay in this browser.

Return to the Python compiler