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
| Category | Question | Impact on Design |
|---|---|---|
| Hardware | Model card reader, keypad, cash dispenser? | Component classes needed |
| Transactions | Which types? Withdraw, deposit, transfer? | Transaction class hierarchy |
| Security | PIN attempt limits? Card retention? | Security logic |
| Cash | Track denominations? Optimal dispensing? | Chain of Responsibility |
| Limits | Daily withdrawal limits? | Validation rules |
| Network | Online/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
| Entity | Responsibilities | Pattern |
|---|---|---|
ATM | Coordinate components, manage state | Context (State Pattern) |
ATMState | Define behavior for each state | State Pattern |
CashDispenser | Dispense bills in optimal denominations | Chain of Responsibility |
Transaction | Encapsulate transaction logic | Command Pattern |
Bank | Verify cards, process transactions | Singleton |
State Transition Diagram
Copy
┌─────────┐ insertCard ┌──────────────┐ validPIN ┌───────────────┐
│ IDLE │──────────────►│ CARD_INSERTED│─────────────►│ AUTHENTICATED │
└─────────┘ └──────────────┘ └───────────────┘
▲ │ │
│ invalidPIN (3x) │
│ │ selectTransaction
│ ▼ ▼
│ ┌──────────────┐ ┌───────────────┐
│ │ CARD_RETAINED│ │ TRANSACTION │
│ └──────────────┘ │ PROCESSING │
│ └───────────────┘
│ │
│ cancel/eject │
└─────────────────────────────────────────────────────────┘
📐 Step 3: Class Diagram
Copy
┌─────────────────────────────────────────────────────────────┐
│ 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
Copy
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
Copy
@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
Copy
@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
Copy
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
Copy
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
Copy
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
Copy
# 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
Why State Pattern for ATM?
Why State Pattern for ATM?
ATM has clear states (Idle, Card Inserted, Authenticated, etc.) with different valid actions in each. State pattern makes transitions explicit and prevents invalid operations.
Why Separate Hardware Components?
Why Separate Hardware Components?
Each component (CardReader, CashDispenser, Screen) has distinct responsibilities and could be swapped independently. This follows Single Responsibility Principle.
Why Greedy Algorithm for Cash Dispensing?
Why Greedy Algorithm for Cash Dispensing?
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.
Why Thread Locking on Withdrawal?
Why Thread Locking on Withdrawal?
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