ATM
medium · transactions, fault tolerance, rollback
Brief-reported interview attribution (unverified): Amazon, Microsoft
How to approach an LLD roundDesign 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
Each test has a five-second execution limit. Your practice drafts and solved progress stay in this browser.
Return to the Python compiler