Skip to main content
Difficulty: 🟢 Beginner-Intermediate | Time: 45 minutes | Patterns: State, Chain of Responsibility, Singleton

🎯 Problem Statement

Design an ATM system that can:
  • Authenticate users with card and PIN
  • Check account balance
  • Withdraw and deposit cash
  • Transfer funds between accounts
  • Handle multiple transaction types
  • Manage cash dispensing units
Why This Problem? ATM is THE showcase for the State Pattern. The ATM behaves differently based on its current state (idle, card inserted, authenticated, etc.). Master this pattern here!

📋 Step 1: Clarify Requirements

Interview Tip: ATM involves both hardware components and banking logic. Clarify the focus!

Questions to Ask the Interviewer

CategoryQuestionImpact on Design
HardwareModel card reader, keypad, cash dispenser?Component classes needed
TransactionsWhich types? Withdraw, deposit, transfer?Transaction class hierarchy
SecurityPIN attempt limits? Card retention?Security logic
CashTrack denominations? Optimal dispensing?Chain of Responsibility
LimitsDaily withdrawal limits?Validation rules
NetworkOnline/offline mode?Bank connectivity

Functional Requirements

  • Insert card and authenticate with PIN (max 3 attempts)
  • Display account balance
  • Withdraw cash (with denomination selection)
  • Deposit cash/checks
  • Transfer between accounts
  • Print receipts
  • Handle card retention after failed attempts

Non-Functional Requirements

  • Secure transactions (encrypted PIN)
  • Handle hardware failures gracefully
  • Maintain transaction logs for audit
  • Support multiple currencies

🧩 Step 2: Identify Core Objects

Key Insight: The ATM uses State Pattern where each state (Idle, CardInserted, Authenticated, etc.) handles user actions differently. Invalid actions for a state can be rejected cleanly.

Hardware

ATM, CardReader, CashDispenser, KeyPad

Banking

Account, Card, Bank, Transaction

Operations

ATMState, Withdrawal, Deposit, Transfer

Entity-Responsibility Mapping

EntityResponsibilitiesPattern
ATMCoordinate components, manage stateContext (State Pattern)
ATMStateDefine behavior for each stateState Pattern
CashDispenserDispense bills in optimal denominationsChain of Responsibility
TransactionEncapsulate transaction logicCommand Pattern
BankVerify cards, process transactionsSingleton

State Transition Diagram

┌─────────┐  insertCard   ┌──────────────┐  validPIN   ┌───────────────┐
│  IDLE   │──────────────►│ CARD_INSERTED│─────────────►│ AUTHENTICATED │
└─────────┘               └──────────────┘              └───────────────┘
     ▲                           │                             │
     │                      invalidPIN (3x)                    │
     │                           │                    selectTransaction
     │                           ▼                             ▼
     │                    ┌──────────────┐            ┌───────────────┐
     │                    │ CARD_RETAINED│            │  TRANSACTION  │
     │                    └──────────────┘            │   PROCESSING  │
     │                                                └───────────────┘
     │                                                         │
     │                        cancel/eject                     │
     └─────────────────────────────────────────────────────────┘

📐 Step 3: Class Diagram

┌─────────────────────────────────────────────────────────────┐
│                           ATM                               │
├─────────────────────────────────────────────────────────────┤
│ - id: String                                                │
│ - location: Address                                         │
│ - state: ATMState                                           │
│ - cardReader: CardReader                                    │
│ - cashDispenser: CashDispenser                              │
│ - keypad: Keypad                                            │
│ - screen: Screen                                            │
│ - currentSession: Session                                   │
├─────────────────────────────────────────────────────────────┤
│ + insertCard(card): void                                    │
│ + authenticatePin(pin): bool                                │
│ + getBalance(): Decimal                                     │
│ + withdraw(amount): bool                                    │
│ + deposit(amount): bool                                     │
│ + transfer(toAccount, amount): bool                         │
│ + ejectCard(): void                                         │
└─────────────────────────────────────────────────────────────┘

            ┌─────────────────┼─────────────────┐
            ▼                 ▼                 ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│    CardReader     │ │  CashDispenser    │ │      Screen       │
├───────────────────┤ ├───────────────────┤ ├───────────────────┤
│ + readCard()      │ │ - cashBins: Map   │ │ + display(msg)    │
│ + ejectCard()     │ │ + dispense(amt)   │ │ + getInput()      │
│ + retainCard()    │ │ + getBalance()    │ │ + showMenu()      │
└───────────────────┘ │ + acceptCash()    │ └───────────────────┘
                      └───────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                       <<abstract>>                          │
│                        ATMState                             │
├─────────────────────────────────────────────────────────────┤
│ + insertCard(atm, card): void                              │
│ + authenticatePin(atm, pin): void                          │
│ + selectTransaction(atm, type): void                       │
│ + executeTransaction(atm): void                            │
│ + cancel(atm): void                                        │
└─────────────────────────────────────────────────────────────┘

   ┌────────┼────────┬────────────────┬────────────────┐
   │        │        │                │                │
┌──┴───┐ ┌──┴───┐ ┌──┴─────────┐ ┌────┴────┐ ┌────────┴────┐
│ Idle │ │ Card │ │Transaction │ │ Process │ │ Out of      │
│State │ │Insert│ │ Select     │ │  ing    │ │ Service     │
└──────┘ └──────┘ └────────────┘ └─────────┘ └─────────────┘

Step 4: Implementation

Enums and Constants

from enum import Enum
from datetime import datetime
from typing import Optional, List, Dict
from dataclasses import dataclass, field
from decimal import Decimal
from abc import ABC, abstractmethod
import uuid
import threading

class TransactionType(Enum):
    BALANCE_INQUIRY = "balance"
    WITHDRAWAL = "withdrawal"
    DEPOSIT = "deposit"
    TRANSFER = "transfer"
    PIN_CHANGE = "pin_change"

class TransactionStatus(Enum):
    PENDING = "pending"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"

class CardStatus(Enum):
    ACTIVE = "active"
    BLOCKED = "blocked"
    EXPIRED = "expired"

# ATM Configuration
MAX_PIN_ATTEMPTS = 3
WITHDRAWAL_LIMIT_DAILY = Decimal("1000.00")
MIN_WITHDRAWAL = Decimal("20.00")
MAX_WITHDRAWAL = Decimal("500.00")

# Available denominations
DENOMINATIONS = [100, 50, 20, 10]

Account and Card Classes

@dataclass
class Account:
    account_number: str
    holder_name: str
    balance: Decimal = Decimal("0.00")
    account_type: str = "checking"  # checking, savings
    daily_withdrawn: Decimal = Decimal("0.00")
    last_withdrawal_date: Optional[datetime] = None
    
    def can_withdraw(self, amount: Decimal) -> bool:
        # Reset daily limit if new day
        today = datetime.now().date()
        if self.last_withdrawal_date and self.last_withdrawal_date.date() != today:
            self.daily_withdrawn = Decimal("0.00")
        
        if amount > self.balance:
            return False
        if self.daily_withdrawn + amount > WITHDRAWAL_LIMIT_DAILY:
            return False
        return True
    
    def withdraw(self, amount: Decimal) -> bool:
        if not self.can_withdraw(amount):
            return False
        
        self.balance -= amount
        self.daily_withdrawn += amount
        self.last_withdrawal_date = datetime.now()
        return True
    
    def deposit(self, amount: Decimal) -> bool:
        if amount <= 0:
            return False
        self.balance += amount
        return True
    
    def transfer_to(self, target: 'Account', amount: Decimal) -> bool:
        if amount > self.balance:
            return False
        
        self.balance -= amount
        target.balance += amount
        return True


class Card:
    def __init__(
        self,
        card_number: str,
        account: Account,
        pin: str,
        expiry_date: datetime
    ):
        self.card_number = card_number
        self.account = account
        self._pin_hash = self._hash_pin(pin)
        self.expiry_date = expiry_date
        self.status = CardStatus.ACTIVE
        self.failed_attempts = 0
    
    def _hash_pin(self, pin: str) -> str:
        # In reality, use proper hashing like bcrypt
        import hashlib
        return hashlib.sha256(pin.encode()).hexdigest()
    
    def verify_pin(self, pin: str) -> bool:
        if self.status != CardStatus.ACTIVE:
            return False
        
        if self._hash_pin(pin) == self._pin_hash:
            self.failed_attempts = 0
            return True
        
        self.failed_attempts += 1
        if self.failed_attempts >= MAX_PIN_ATTEMPTS:
            self.status = CardStatus.BLOCKED
        return False
    
    def change_pin(self, old_pin: str, new_pin: str) -> bool:
        if not self.verify_pin(old_pin):
            return False
        
        if len(new_pin) != 4 or not new_pin.isdigit():
            return False
        
        self._pin_hash = self._hash_pin(new_pin)
        return True
    
    def is_expired(self) -> bool:
        return datetime.now() > self.expiry_date

Transaction Classes

@dataclass
class Transaction:
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:12].upper())
    transaction_type: TransactionType = TransactionType.BALANCE_INQUIRY
    amount: Decimal = Decimal("0.00")
    source_account: Optional[str] = None
    target_account: Optional[str] = None
    timestamp: datetime = field(default_factory=datetime.now)
    status: TransactionStatus = TransactionStatus.PENDING
    atm_id: str = ""
    
    def complete(self):
        self.status = TransactionStatus.COMPLETED
    
    def fail(self):
        self.status = TransactionStatus.FAILED
    
    def __str__(self):
        return (f"[{self.id}] {self.transaction_type.value}: "
                f"${self.amount} - {self.status.value}")

Hardware Components

class CardReader:
    def __init__(self):
        self.card: Optional[Card] = None
        self._card_retained = False
    
    def read_card(self, card: Card) -> bool:
        if card.is_expired():
            print("Card expired!")
            return False
        
        if card.status == CardStatus.BLOCKED:
            print("Card is blocked!")
            self.retain_card()
            return False
        
        self.card = card
        print(f"Card {card.card_number[-4:]} inserted")
        return True
    
    def eject_card(self) -> Optional[Card]:
        if self._card_retained:
            print("Card has been retained!")
            return None
        
        card = self.card
        self.card = None
        print("Card ejected")
        return card
    
    def retain_card(self):
        """Retain card (too many failed attempts)"""
        self._card_retained = True
        print("Card has been retained by ATM. Please contact your bank.")


class CashBin:
    def __init__(self, denomination: int, initial_count: int = 100):
        self.denomination = denomination
        self.count = initial_count
    
    def can_dispense(self, num_notes: int) -> bool:
        return self.count >= num_notes
    
    def dispense(self, num_notes: int) -> bool:
        if not self.can_dispense(num_notes):
            return False
        self.count -= num_notes
        return True
    
    def add(self, num_notes: int):
        self.count += num_notes


class CashDispenser:
    def __init__(self):
        self.bins: Dict[int, CashBin] = {
            100: CashBin(100, 50),
            50: CashBin(50, 100),
            20: CashBin(20, 200),
            10: CashBin(10, 300)
        }
    
    def get_total_cash(self) -> Decimal:
        total = sum(
            bin.denomination * bin.count 
            for bin in self.bins.values()
        )
        return Decimal(str(total))
    
    def can_dispense(self, amount: Decimal) -> bool:
        """Check if amount can be dispensed with available notes"""
        if amount > self.get_total_cash():
            return False
        
        # Try to find a valid combination
        return self._find_dispense_combination(int(amount)) is not None
    
    def _find_dispense_combination(self, amount: int) -> Optional[Dict[int, int]]:
        """Find optimal note combination using greedy algorithm"""
        result = {}
        remaining = amount
        
        for denom in sorted(self.bins.keys(), reverse=True):
            if remaining <= 0:
                break
            
            notes_needed = remaining // denom
            notes_available = self.bins[denom].count
            notes_to_use = min(notes_needed, notes_available)
            
            if notes_to_use > 0:
                result[denom] = notes_to_use
                remaining -= denom * notes_to_use
        
        return result if remaining == 0 else None
    
    def dispense(self, amount: Decimal) -> Optional[Dict[int, int]]:
        """Dispense cash and return note breakdown"""
        combination = self._find_dispense_combination(int(amount))
        
        if not combination:
            print("Cannot dispense exact amount!")
            return None
        
        # Dispense notes
        for denom, count in combination.items():
            self.bins[denom].dispense(count)
        
        print(f"Dispensing ${amount}:")
        for denom, count in sorted(combination.items(), reverse=True):
            if count > 0:
                print(f"  {count} x ${denom}")
        
        return combination
    
    def accept_cash(self, notes: Dict[int, int]) -> Decimal:
        """Accept deposited cash"""
        total = Decimal("0.00")
        
        for denom, count in notes.items():
            if denom in self.bins:
                self.bins[denom].add(count)
                total += Decimal(str(denom * count))
        
        return total


class Screen:
    def display(self, message: str):
        print(f"\n{'='*40}")
        print(message)
        print('='*40)
    
    def show_menu(self, options: List[str]) -> int:
        self.display("Please select an option:")
        for i, option in enumerate(options, 1):
            print(f"  {i}. {option}")
        print()
        
        # In real ATM, would read from keypad
        # Here we simulate with input
        try:
            choice = int(input("Enter choice: "))
            return choice if 1 <= choice <= len(options) else -1
        except ValueError:
            return -1
    
    def get_amount(self) -> Optional[Decimal]:
        try:
            amount = input("Enter amount: $")
            return Decimal(amount)
        except:
            return None
    
    def print_receipt(self, transaction: Transaction, balance: Decimal):
        print("\n" + "="*40)
        print("           TRANSACTION RECEIPT")
        print("="*40)
        print(f"Date: {transaction.timestamp.strftime('%Y-%m-%d %H:%M')}")
        print(f"Transaction ID: {transaction.id}")
        print(f"Type: {transaction.transaction_type.value.upper()}")
        if transaction.amount > 0:
            print(f"Amount: ${transaction.amount}")
        print(f"Balance: ${balance}")
        print("="*40)
        print("Thank you for using our ATM!")
        print("="*40 + "\n")

ATM State Pattern

class ATMState(ABC):
    """State pattern for ATM operations"""
    
    @abstractmethod
    def insert_card(self, atm: 'ATM', card: Card):
        pass
    
    @abstractmethod
    def authenticate_pin(self, atm: 'ATM', pin: str):
        pass
    
    @abstractmethod
    def select_transaction(self, atm: 'ATM', trans_type: TransactionType):
        pass
    
    @abstractmethod
    def execute_transaction(self, atm: 'ATM', **kwargs):
        pass
    
    @abstractmethod
    def cancel(self, atm: 'ATM'):
        pass


class IdleState(ATMState):
    """ATM waiting for card"""
    
    def insert_card(self, atm: 'ATM', card: Card):
        if atm.card_reader.read_card(card):
            atm.current_card = card
            atm.state = CardInsertedState()
            atm.screen.display("Card accepted. Please enter your PIN.")
        else:
            atm.screen.display("Card not accepted.")
    
    def authenticate_pin(self, atm: 'ATM', pin: str):
        atm.screen.display("Please insert card first.")
    
    def select_transaction(self, atm: 'ATM', trans_type: TransactionType):
        atm.screen.display("Please insert card first.")
    
    def execute_transaction(self, atm: 'ATM', **kwargs):
        atm.screen.display("Please insert card first.")
    
    def cancel(self, atm: 'ATM'):
        atm.screen.display("No active session.")


class CardInsertedState(ATMState):
    """Card inserted, waiting for PIN"""
    
    def insert_card(self, atm: 'ATM', card: Card):
        atm.screen.display("Card already inserted.")
    
    def authenticate_pin(self, atm: 'ATM', pin: str):
        if atm.current_card.verify_pin(pin):
            atm.state = AuthenticatedState()
            atm.screen.display("PIN verified. Select transaction.")
        else:
            remaining = MAX_PIN_ATTEMPTS - atm.current_card.failed_attempts
            if remaining > 0:
                atm.screen.display(f"Incorrect PIN. {remaining} attempts remaining.")
            else:
                atm.card_reader.retain_card()
                atm.screen.display("Card retained. Contact your bank.")
                atm.state = IdleState()
    
    def select_transaction(self, atm: 'ATM', trans_type: TransactionType):
        atm.screen.display("Please enter PIN first.")
    
    def execute_transaction(self, atm: 'ATM', **kwargs):
        atm.screen.display("Please enter PIN first.")
    
    def cancel(self, atm: 'ATM'):
        atm.card_reader.eject_card()
        atm.current_card = None
        atm.state = IdleState()
        atm.screen.display("Transaction cancelled.")


class AuthenticatedState(ATMState):
    """User authenticated, can select transactions"""
    
    def insert_card(self, atm: 'ATM', card: Card):
        atm.screen.display("Session active. Complete or cancel first.")
    
    def authenticate_pin(self, atm: 'ATM', pin: str):
        atm.screen.display("Already authenticated.")
    
    def select_transaction(self, atm: 'ATM', trans_type: TransactionType):
        atm.current_transaction_type = trans_type
        atm.state = TransactionSelectedState()
        
        if trans_type == TransactionType.BALANCE_INQUIRY:
            atm.state.execute_transaction(atm)
        else:
            atm.screen.display(f"Selected: {trans_type.value}. Enter details.")
    
    def execute_transaction(self, atm: 'ATM', **kwargs):
        atm.screen.display("Please select transaction type first.")
    
    def cancel(self, atm: 'ATM'):
        atm.card_reader.eject_card()
        atm.current_card = None
        atm.current_transaction_type = None
        atm.state = IdleState()
        atm.screen.display("Session ended. Thank you!")


class TransactionSelectedState(ATMState):
    """Transaction type selected, ready to execute"""
    
    def insert_card(self, atm: 'ATM', card: Card):
        atm.screen.display("Session active.")
    
    def authenticate_pin(self, atm: 'ATM', pin: str):
        atm.screen.display("Already authenticated.")
    
    def select_transaction(self, atm: 'ATM', trans_type: TransactionType):
        atm.current_transaction_type = trans_type
        atm.screen.display(f"Changed to: {trans_type.value}")
    
    def execute_transaction(self, atm: 'ATM', **kwargs):
        trans_type = atm.current_transaction_type
        account = atm.current_card.account
        
        transaction = Transaction(
            transaction_type=trans_type,
            source_account=account.account_number,
            atm_id=atm.id
        )
        
        success = False
        
        if trans_type == TransactionType.BALANCE_INQUIRY:
            success = True
            atm.screen.display(f"Current Balance: ${account.balance}")
        
        elif trans_type == TransactionType.WITHDRAWAL:
            amount = kwargs.get('amount')
            if amount and atm._process_withdrawal(account, amount):
                transaction.amount = amount
                success = True
        
        elif trans_type == TransactionType.DEPOSIT:
            amount = kwargs.get('amount')
            if amount and atm._process_deposit(account, amount):
                transaction.amount = amount
                success = True
        
        elif trans_type == TransactionType.TRANSFER:
            amount = kwargs.get('amount')
            target = kwargs.get('target_account')
            if amount and target and atm._process_transfer(account, target, amount):
                transaction.amount = amount
                transaction.target_account = target.account_number
                success = True
        
        if success:
            transaction.complete()
            atm.transaction_log.append(transaction)
            atm.screen.print_receipt(transaction, account.balance)
        else:
            transaction.fail()
        
        # Return to authenticated state for another transaction
        atm.state = AuthenticatedState()
    
    def cancel(self, atm: 'ATM'):
        atm.current_transaction_type = None
        atm.state = AuthenticatedState()
        atm.screen.display("Transaction cancelled. Select another or exit.")


class OutOfServiceState(ATMState):
    """ATM out of service"""
    
    def insert_card(self, atm: 'ATM', card: Card):
        atm.screen.display("ATM out of service. Please use another ATM.")
    
    def authenticate_pin(self, atm: 'ATM', pin: str):
        atm.screen.display("ATM out of service.")
    
    def select_transaction(self, atm: 'ATM', trans_type: TransactionType):
        atm.screen.display("ATM out of service.")
    
    def execute_transaction(self, atm: 'ATM', **kwargs):
        atm.screen.display("ATM out of service.")
    
    def cancel(self, atm: 'ATM'):
        atm.screen.display("ATM out of service.")

ATM Main Class

class ATM:
    def __init__(self, atm_id: str, bank: 'Bank'):
        self.id = atm_id
        self.bank = bank
        
        # Hardware components
        self.card_reader = CardReader()
        self.cash_dispenser = CashDispenser()
        self.screen = Screen()
        
        # State management
        self.state: ATMState = IdleState()
        self.current_card: Optional[Card] = None
        self.current_transaction_type: Optional[TransactionType] = None
        
        # Transaction log
        self.transaction_log: List[Transaction] = []
        
        self._lock = threading.Lock()
    
    # Delegate to current state
    def insert_card(self, card: Card):
        self.state.insert_card(self, card)
    
    def authenticate_pin(self, pin: str):
        self.state.authenticate_pin(self, pin)
    
    def select_transaction(self, trans_type: TransactionType):
        self.state.select_transaction(self, trans_type)
    
    def execute_transaction(self, **kwargs):
        self.state.execute_transaction(self, **kwargs)
    
    def cancel(self):
        self.state.cancel(self)
    
    # Transaction processing methods
    def _process_withdrawal(self, account: Account, amount: Decimal) -> bool:
        with self._lock:
            # Validation
            if amount < MIN_WITHDRAWAL:
                self.screen.display(f"Minimum withdrawal is ${MIN_WITHDRAWAL}")
                return False
            
            if amount > MAX_WITHDRAWAL:
                self.screen.display(f"Maximum withdrawal is ${MAX_WITHDRAWAL}")
                return False
            
            if not self.cash_dispenser.can_dispense(amount):
                self.screen.display("Cannot dispense this amount. Try different amount.")
                return False
            
            if not account.can_withdraw(amount):
                self.screen.display("Insufficient funds or daily limit exceeded.")
                return False
            
            # Execute withdrawal
            if account.withdraw(amount):
                self.cash_dispenser.dispense(amount)
                self.screen.display(f"Please take your cash: ${amount}")
                return True
            
            return False
    
    def _process_deposit(self, account: Account, amount: Decimal) -> bool:
        if amount <= 0:
            self.screen.display("Invalid amount!")
            return False
        
        # In reality, would count deposited bills
        notes = {50: int(amount / 50)}  # Simplified
        deposited = self.cash_dispenser.accept_cash(notes)
        
        if account.deposit(deposited):
            self.screen.display(f"Deposited: ${deposited}")
            return True
        
        return False
    
    def _process_transfer(
        self, 
        from_account: Account, 
        to_account: Account, 
        amount: Decimal
    ) -> bool:
        if amount <= 0:
            self.screen.display("Invalid amount!")
            return False
        
        if from_account.transfer_to(to_account, amount):
            self.screen.display(f"Transferred ${amount} to {to_account.account_number}")
            return True
        
        self.screen.display("Transfer failed. Insufficient funds.")
        return False
    
    def check_cash_level(self) -> bool:
        """Check if ATM needs refilling"""
        total = self.cash_dispenser.get_total_cash()
        if total < Decimal("5000"):
            self.state = OutOfServiceState()
            return False
        return True
    
    def run_interactive(self):
        """Run ATM in interactive mode"""
        self.screen.display("Welcome to the ATM!")
        
        while True:
            if isinstance(self.state, IdleState):
                card_num = input("\nInsert card number (or 'quit'): ")
                if card_num.lower() == 'quit':
                    break
                
                card = self.bank.get_card(card_num)
                if card:
                    self.insert_card(card)
                else:
                    self.screen.display("Card not found!")
            
            elif isinstance(self.state, CardInsertedState):
                pin = input("Enter PIN: ")
                self.authenticate_pin(pin)
            
            elif isinstance(self.state, AuthenticatedState):
                options = ["Balance Inquiry", "Withdraw", "Deposit", "Transfer", "Exit"]
                choice = self.screen.show_menu(options)
                
                if choice == 1:
                    self.select_transaction(TransactionType.BALANCE_INQUIRY)
                elif choice == 2:
                    self.select_transaction(TransactionType.WITHDRAWAL)
                    amount = self.screen.get_amount()
                    if amount:
                        self.execute_transaction(amount=amount)
                elif choice == 3:
                    self.select_transaction(TransactionType.DEPOSIT)
                    amount = self.screen.get_amount()
                    if amount:
                        self.execute_transaction(amount=amount)
                elif choice == 4:
                    self.select_transaction(TransactionType.TRANSFER)
                    target_num = input("Enter target account: ")
                    target = self.bank.get_account(target_num)
                    if target:
                        amount = self.screen.get_amount()
                        if amount:
                            self.execute_transaction(amount=amount, target_account=target)
                elif choice == 5:
                    self.cancel()
            
            else:
                break


class Bank:
    """Simplified bank for demo purposes"""
    
    def __init__(self, name: str):
        self.name = name
        self.accounts: Dict[str, Account] = {}
        self.cards: Dict[str, Card] = {}
    
    def create_account(self, holder_name: str, initial_balance: Decimal) -> Account:
        account = Account(
            account_number=f"ACC{len(self.accounts)+1:06d}",
            holder_name=holder_name,
            balance=initial_balance
        )
        self.accounts[account.account_number] = account
        return account
    
    def issue_card(self, account: Account, pin: str) -> Card:
        card = Card(
            card_number=f"CARD{len(self.cards)+1:012d}",
            account=account,
            pin=pin,
            expiry_date=datetime(2027, 12, 31)
        )
        self.cards[card.card_number] = card
        return card
    
    def get_card(self, card_number: str) -> Optional[Card]:
        return self.cards.get(card_number)
    
    def get_account(self, account_number: str) -> Optional[Account]:
        return self.accounts.get(account_number)

Step 5: Usage Example

# Setup bank and accounts
bank = Bank("National Bank")

# Create accounts
account1 = bank.create_account("John Doe", Decimal("5000.00"))
account2 = bank.create_account("Jane Smith", Decimal("3000.00"))

# Issue cards
card1 = bank.issue_card(account1, "1234")
card2 = bank.issue_card(account2, "5678")

print(f"John's Card: {card1.card_number}")
print(f"Jane's Card: {card2.card_number}")

# Create ATM
atm = ATM("ATM001", bank)

# Simulate transactions
print("\n--- John's Session ---")

# Insert card
atm.insert_card(card1)

# Enter PIN
atm.authenticate_pin("1234")

# Check balance
atm.select_transaction(TransactionType.BALANCE_INQUIRY)

# Withdraw cash
atm.select_transaction(TransactionType.WITHDRAWAL)
atm.execute_transaction(amount=Decimal("200.00"))

# Transfer to Jane
atm.select_transaction(TransactionType.TRANSFER)
atm.execute_transaction(amount=Decimal("500.00"), target_account=account2)

# Exit
atm.cancel()

print(f"\nJohn's final balance: ${account1.balance}")
print(f"Jane's final balance: ${account2.balance}")

# Check ATM cash level
print(f"\nATM Cash: ${atm.cash_dispenser.get_total_cash()}")

Key Design Decisions

ATM has clear states (Idle, Card Inserted, Authenticated, etc.) with different valid actions in each. State pattern makes transitions explicit and prevents invalid operations.
Each component (CardReader, CashDispenser, Screen) has distinct responsibilities and could be swapped independently. This follows Single Responsibility Principle.
Greedy works well for standard denominations (100, 50, 20, 10). For some edge cases, dynamic programming might be needed, but greedy is simpler and faster.
Multiple processes could access the ATM simultaneously (hardware interrupts, network requests). Locking prevents race conditions on cash counts and account balances.

Extension Points

Interview Extensions - Be ready to discuss:
  • Multi-Account Cards: Support cards linked to multiple accounts
  • Mini Statement: Show last N transactions
  • Bill Payments: Pay utilities from ATM
  • Cardless Withdrawal: OTP-based withdrawal
  • Fraud Detection: Unusual patterns, geographic anomalies