An ATM (Automated Teller Machine) is a self-service banking machine that allows users to perform basic financial transactions such as withdrawing cash and checking account balances using a debit or credit card and a secure PIN, without needing to visit a bank branch.
Secure Banking 24/7
Please insert your card to begin
CASH DISPENSER
INSERT CARD
RECEIPT
They interface with backend banking systems to verify account details, authenticate users, and update balances in real-time.
In this chapter, we will explore the low-level design of ATM in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: What types of transactions should the ATM support?
Interviewer: The ATM should support cash withdrawal, deposit and balance inquiry.
Candidate: Should the ATM communicate with a central bank server to validate credentials, fetch account data, and perform transactions?
Interviewer: Yes, the ATM acts as a client. It should authenticate user card details and perform all account-related operations. For simplicity, you can assume this is a local call rather than a remote one.
Candidate: Can a user perform multiple transactions per session?
Interviewer: To keep things simple, assume only one transaction per session. The card should be ejected after each transaction.
Candidate: Do we need to support multiple account types per user, such as checking and savings?
Interviewer: No, let’s assume each user has only one account. However, a single account may be linked to multiple ATM cards.
Candidate: Should the ATM maintain an inventory of cash by denomination?
Interviewer: Yes. The ATM must maintain an internal inventory of cash and should only allow withdrawals if sufficient cash is available in the appropriate denominations.
Candidate: When dispensing cash, should the ATM prioritize certain denominations—for example, prefer higher denominations first?
Interviewer: Yes, it should try to dispense the requested amount using the highest available denominations first.
Candidate: Should we enforce daily transaction or withdrawal limits per user?
Interviewer: Let’s skip that for now. Assume there are no limits on the number or amount of transactions per day.
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, lets identify the core entities/objects we will have in our system.
Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.
Let’s walk through the functional requirements and extract the relevant entities:
This implies the need for two entities: Card and Account. The Card encapsulates the card number and PIN, and is linked to an Account that contains user balance and account details.
This suggests the need for a BankService entity to simulate backend operations. It will expose methods to authenticate cards, get account balance, debit/credit funds, etc.
This indicates an ATMMachine entity that holds the cash and manages internal state. It should contain a NoteDispenser component, which handles the dispensing logic.
These core entities define the key abstractions of the ATM system and will guide the structure of our object-oriented design and class diagrams.
OperationTypeAn enumeration that defines the types of transactions a user can select (e.g., CHECK_BALANCE, WITHDRAW_CASH).
This improves type safety and code readability when handling user menu selections.
CardA simple data class representing a user's debit card.
It holds the card number and PIN, acting as a data transfer object.
AccountRepresents a user's bank account.
It stores the account number and the current balance. It includes synchronized methods (deposit, withdraw) to ensure thread-safe balance updates.
ATMSystemThe central class of the system.
It acts as a Facade and Singleton, providing a single entry point for all user interactions. It manages the ATM's state and coordinates with all other components.
BankServiceA mock service that simulates the connection to a bank's backend.
It is responsible for authenticating cards and processing transactions by linking cards to accounts and executing deposits or withdrawals.
The relationships between classes define the system's structure and data flow.
This "has-a" relationship implies ownership, where an object is composed of other objects.
ATM has a BankService: The ATM owns an instance of the bank service to process transactions.CashDispenser has a DispenseChain: It holds the head of the note dispenser chain.NoteDispenser has a DispenseChain: Each link in the chain holds a reference (nextChain) to the next link.This is a weaker "uses-a" relationship where objects are related but don't have strong ownership.
ATM is associated with an ATMState: The currentState of the ATM determines its behavior. This relationship is dynamic and changes as the user progresses through a transaction.ATM is associated with a Card: The currentCard field holds the card being used for the current session.BankService maintains an association between a Card and an Account. This mapping is crucial for retrieving account details based on the inserted card.This "is-a" relationship defines a type hierarchy.
IdleState, HasCardState, and AuthenticatedState implement the ATMState interface.NoteDispenser implements the DispenseChain interface.NoteDispenser100, NoteDispenser50, and NoteDispenser20 extend the abstract NoteDispenser class.Several design patterns are employed to create a robust, maintainable, and extensible system.
The ATM's session management is built using the State pattern. The ATM class delegates its behavior to a currentState object (an implementation of ATMState). When a user performs an action (e.g., inserting a card), the method call is forwarded to the current state object. The state objects (IdleState, HasCardState, etc.) handle the logic and are responsible for transitioning the ATM to the next state. This keeps the ATM class clean and makes it easy to add or modify states without changing core logic.
This pattern is used to process cash withdrawal requests. The NoteDispenser objects are linked in a chain, from the highest denomination to the lowest. When a withdrawal is requested, the amount is passed to the head of the chain (NoteDispenser100). Each dispenser handles the amount with its own denomination and passes the remaining amount to the next dispenser in the chain. This decouples the withdrawal logic and makes it easy to add new note denominations by simply adding a new link to the chain.
The ATM class acts as a Facade. It provides a simple, high-level interface (insertCard(), enterPin(), selectOperation()) to the client (the ATMDemo class). This facade hides the complex internal workings of the state machine, the bank service interactions, and the cash dispensing subsystem, simplifying the client's interaction with the system.
The ATM class is implemented as a Singleton to ensure that only one instance of the ATM controller exists throughout the application's lifecycle. This is logical, as the software is designed to operate a single physical machine. It's achieved through a private constructor and a static getInstance() method.
OperationType Enum1class OperationType(Enum):
2 CHECK_BALANCE = "CHECK_BALANCE"
3 WITHDRAW_CASH = "WITHDRAW_CASH"
4 DEPOSIT_CASH = "DEPOSIT_CASH"Enumerates the user-facing operations supported by the ATM interface. This provides clean routing for actions based on user selection.
Card ClassModels a physical debit card, uniquely identified by a card number and protected by a PIN for authentication.
1class Card:
2 def __init__(self, card_number: str, pin: str):
3 self._card_number = card_number
4 self._pin = pin
5
6 def get_card_number(self) -> str:
7 return self._card_number
8
9 def get_pin(self) -> str:
10 return self._pinAccount ClassRepresents a bank account linked to one or more cards.
1class Account:
2 def __init__(self, account_number: str, balance: float):
3 self._account_number = account_number
4 self._balance = balance
5 self._cards: Dict[str, Card] = {}
6 self._lock = threading.Lock()
7
8 def get_account_number(self) -> str:
9 return self._account_number
10
11 def get_balance(self) -> float:
12 return self._balance
13
14 def get_cards(self) -> Dict[str, Card]:
15 return self._cards
16
17 def deposit(self, amount: float):
18 with self._lock:
19 self._balance += amount
20
21 def withdraw(self, amount: float) -> bool:
22 with self._lock:
23 if self._balance >= amount:
24 self._balance -= amount
25 return True
26 return FalseSynchronization ensures safe updates to balance during concurrent transactions.
BankService Class1class BankService:
2 def __init__(self):
3 self._accounts: Dict[str, Account] = {}
4 self._cards: Dict[str, Card] = {}
5 self._card_account_map: Dict[Card, Account] = {}
6
7 # Create sample accounts and cards
8 account1 = self.create_account("1234567890", 1000.0)
9 card1 = self.create_card("1234-5678-9012-3456", "1234")
10 self.link_card_to_account(card1, account1)
11
12 account2 = self.create_account("9876543210", 500.0)
13 card2 = self.create_card("9876-5432-1098-7654", "4321")
14 self.link_card_to_account(card2, account2)
15
16 def create_account(self, account_number: str, initial_balance: float) -> Account:
17 account = Account(account_number, initial_balance)
18 self._accounts[account_number] = account
19 return account
20
21 def create_card(self, card_number: str, pin: str) -> Card:
22 card = Card(card_number, pin)
23 self._cards[card_number] = card
24 return card
25
26 def authenticate(self, card: Card, pin: str) -> bool:
27 return card.get_pin() == pin
28
29 def authenticate_card(self, card_number: str) -> Optional[Card]:
30 return self._cards.get(card_number)
31
32 def get_balance(self, card: Card) -> float:
33 return self._card_account_map[card].get_balance()
34
35 def withdraw_money(self, card: Card, amount: float):
36 self._card_account_map[card].withdraw(amount)
37
38 def deposit_money(self, card: Card, amount: float):
39 self._card_account_map[card].deposit(amount)
40
41 def link_card_to_account(self, card: Card, account: Account):
42 account.get_cards()[card.get_card_number()] = card
43 self._card_account_map[card] = accountA mock back-end service that handles:
It maintains mappings between cards and accounts and enforces account integrity.
Dispensing a specific cash amount requires breaking it down into available note denominations (e.g., $100, $50, $20). The Chain of Responsibility pattern is a perfect fit for this, creating a flexible and extensible system for dispensing notes.
1class DispenseChain(ABC):
2 @abstractmethod
3 def set_next_chain(self, next_chain: 'DispenseChain'):
4 pass
5
6 @abstractmethod
7 def dispense(self, amount: int):
8 pass
9
10 @abstractmethod
11 def can_dispense(self, amount: int) -> bool:
12 pass1class NoteDispenser(DispenseChain):
2 def __init__(self, note_value: int, num_notes: int):
3 self._note_value = note_value
4 self._num_notes = num_notes
5 self._next_chain: Optional[DispenseChain] = None
6 self._lock = threading.Lock()
7
8 def set_next_chain(self, next_chain: DispenseChain):
9 self._next_chain = next_chain
10
11 def dispense(self, amount: int):
12 with self._lock:
13 if amount >= self._note_value:
14 num_to_dispense = min(amount // self._note_value, self._num_notes)
15 remaining_amount = amount - (num_to_dispense * self._note_value)
16
17 if num_to_dispense > 0:
18 print(f"Dispensing {num_to_dispense} x ${self._note_value} note(s)")
19 self._num_notes -= num_to_dispense
20
21 if remaining_amount > 0 and self._next_chain is not None:
22 self._next_chain.dispense(remaining_amount)
23 elif self._next_chain is not None:
24 self._next_chain.dispense(amount)
25
26 def can_dispense(self, amount: int) -> bool:
27 with self._lock:
28 if amount < 0:
29 return False
30 if amount == 0:
31 return True
32
33 num_to_use = min(amount // self._note_value, self._num_notes)
34 remaining_amount = amount - (num_to_use * self._note_value)
35
36 if remaining_amount == 0:
37 return True
38 if self._next_chain is not None:
39 return self._next_chain.can_dispense(remaining_amount)
40 return FalseThe DispenseChain interface establishes a common contract for all links.
The NoteDispenser abstract class contains the shared logic:
This design is highly modular. To add a new note denomination (e.g., $10), we would simply create a new NoteDispenser10 class and insert it into the chain, with no changes to existing code.
Specialized implementations of NoteDispenser for different note denominations. These are the specific links in our chain, each responsible for a single note denomination.
1class NoteDispenser20(NoteDispenser):
2 def __init__(self, num_notes: int):
3 super().__init__(20, num_notes)
4
5class NoteDispenser50(NoteDispenser):
6 def __init__(self, num_notes: int):
7 super().__init__(50, num_notes)
8
9class NoteDispenser100(NoteDispenser):
10 def __init__(self, num_notes: int):
11 super().__init__(100, num_notes)Each class is a simple extension of NoteDispenser, specifying its note value.
This design:
CashDispenser FacadeThe CashDispenser class acts as a client for the chain, initiating the dispensing process by passing the request to the first link.
1class CashDispenser:
2 def __init__(self, chain: DispenseChain):
3 self._chain = chain
4 self._lock = threading.Lock()
5
6 def dispense_cash(self, amount: int):
7 with self._lock:
8 self._chain.dispense(amount)
9
10 def can_dispense_cash(self, amount: int) -> bool:
11 with self._lock:
12 if amount % 10 != 0:
13 return False
14 return self._chain.can_dispense(amount)ATMState and ImplementationsATMState InterfaceDefines different states of the ATM session: Idle, HasCard, and Authenticated. Each state governs what actions are allowed.
1class ATMState(ABC):
2 @abstractmethod
3 def insert_card(self, atm: 'ATM', card_number: str):
4 pass
5
6 @abstractmethod
7 def enter_pin(self, atm: 'ATM', pin: str):
8 pass
9
10 @abstractmethod
11 def select_operation(self, atm: 'ATM', op: OperationType, *args):
12 pass
13
14 @abstractmethod
15 def eject_card(self, atm: 'ATM'):
16 passIdleState1class IdleState(ATMState):
2 def insert_card(self, atm: 'ATM', card_number: str):
3 print("\nCard has been inserted.")
4 card = atm.get_bank_service().authenticate_card(card_number)
5
6 if card is None:
7 self.eject_card(atm)
8 else:
9 atm.set_current_card(card)
10 atm.change_state(HasCardState())
11
12 def enter_pin(self, atm: 'ATM', pin: str):
13 print("Error: Please insert a card first.")
14
15 def select_operation(self, atm: 'ATM', op: OperationType, *args):
16 print("Error: Please insert a card first.")
17
18 def eject_card(self, atm: 'ATM'):
19 print("Error: Card not found.")
20 atm.set_current_card(None)Initial state when ATM is idle. Only card insertion is valid here. Transitions to HasCardState upon success.
HasCardState1class HasCardState(ATMState):
2 def insert_card(self, atm: 'ATM', card_number: str):
3 print("Error: A card is already inserted. Cannot insert another card.")
4
5 def enter_pin(self, atm: 'ATM', pin: str):
6 print("Authenticating PIN...")
7 card = atm.get_current_card()
8 is_authenticated = atm.get_bank_service().authenticate(card, pin)
9
10 if is_authenticated:
11 print("Authentication successful.")
12 atm.change_state(AuthenticatedState())
13 else:
14 print("Authentication failed: Incorrect PIN.")
15 self.eject_card(atm)
16
17 def select_operation(self, atm: 'ATM', op: OperationType, *args):
18 print("Error: Please enter your PIN first to select an operation.")
19
20 def eject_card(self, atm: 'ATM'):
21 print("Card has been ejected. Thank you for using our ATM.")
22 atm.set_current_card(None)
23 atm.change_state(IdleState())Waits for PIN input. Validates the PIN and moves to AuthenticatedState if correct, else ejects the card.
1class AuthenticatedState(ATMState):
2 def insert_card(self, atm: 'ATM', card_number: str):
3 print("Error: A card is already inserted and a session is active.")
4
5 def enter_pin(self, atm: 'ATM', pin: str):
6 print("Error: PIN has already been entered and authenticated.")
7
8 def select_operation(self, atm: 'ATM', op: OperationType, *args):
9 if op == OperationType.CHECK_BALANCE:
10 atm.check_balance()
11 elif op == OperationType.WITHDRAW_CASH:
12 if len(args) == 0 or args[0] <= 0:
13 print("Error: Invalid withdrawal amount specified.")
14 return
15
16 amount_to_withdraw = args[0]
17 account_balance = atm.get_bank_service().get_balance(atm.get_current_card())
18
19 if amount_to_withdraw > account_balance:
20 print("Error: Insufficient balance.")
21 return
22
23 print(f"Processing withdrawal for ${amount_to_withdraw}")
24 atm.withdraw_cash(amount_to_withdraw)
25 elif op == OperationType.DEPOSIT_CASH:
26 if len(args) == 0 or args[0] <= 0:
27 print("Error: Invalid deposit amount specified.")
28 return
29
30 amount_to_deposit = args[0]
31 print(f"Processing deposit for ${amount_to_deposit}")
32 atm.deposit_cash(amount_to_deposit)
33 else:
34 print("Error: Invalid operation selected.")
35 return
36
37 # End the session after one transaction
38 print("Transaction complete.")
39 self.eject_card(atm)
40
41 def eject_card(self, atm: 'ATM'):
42 print("Ending session. Card has been ejected. Thank you for using our ATM.")
43 atm.set_current_card(None)
44 atm.change_state(IdleState())Allows all operations. After any operation, session ends by calling ejectCard.
ATM Class (Facade)The ATM class orchestrates all the subsystems. It uses the Singleton Pattern to ensure only one instance of the physical ATM exists and the Facade Pattern to provide a simple, unified interface to its complex internal operations.
1class ATM:
2 _instance = None
3 _lock = threading.Lock()
4
5 def __new__(cls):
6 if cls._instance is None:
7 with cls._lock:
8 if cls._instance is None:
9 cls._instance = super().__new__(cls)
10 cls._instance._initialized = False
11 return cls._instance
12
13 def __init__(self):
14 if not self._initialized:
15 self._current_state = IdleState()
16 self._bank_service = BankService()
17 self._current_card: Optional[Card] = None
18 self._transaction_counter = 0
19
20 # Setup the dispenser chain
21 c1 = NoteDispenser100(10) # 10 x $100 notes
22 c2 = NoteDispenser50(20) # 20 x $50 notes
23 c3 = NoteDispenser20(30) # 30 x $20 notes
24 c1.set_next_chain(c2)
25 c2.set_next_chain(c3)
26 self._cash_dispenser = CashDispenser(c1)
27 self._initialized = True
28
29 @classmethod
30 def get_instance(cls):
31 return cls()
32
33 def change_state(self, new_state: ATMState):
34 self._current_state = new_state
35
36 def set_current_card(self, card: Optional[Card]):
37 self._current_card = card
38
39 def insert_card(self, card_number: str):
40 self._current_state.insert_card(self, card_number)
41
42 def enter_pin(self, pin: str):
43 self._current_state.enter_pin(self, pin)
44
45 def select_operation(self, op: OperationType, *args):
46 self._current_state.select_operation(self, op, *args)
47
48 def check_balance(self):
49 balance = self._bank_service.get_balance(self._current_card)
50 print(f"Your current account balance is: ${balance:.2f}")
51
52 def withdraw_cash(self, amount: int):
53 if not self._cash_dispenser.can_dispense_cash(amount):
54 raise RuntimeError("Insufficient cash available in the ATM.")
55
56 self._bank_service.withdraw_money(self._current_card, amount)
57
58 try:
59 self._cash_dispenser.dispense_cash(amount)
60 except Exception as e:
61 self._bank_service.deposit_money(self._current_card, amount) # Deposit back if dispensing fails
62 raise e
63
64 def deposit_cash(self, amount: int):
65 self._bank_service.deposit_money(self._current_card, amount)
66
67 def get_current_card(self) -> Optional[Card]:
68 return self._current_card
69
70 def get_bank_service(self) -> BankService:
71 return self._bank_serviceATMState)BankService and CashDispenserThe ATMDemo class simulates a user interacting with the ATM, demonstrating various user flows and edge cases.
1class ATMDemo:
2 @staticmethod
3 def main():
4 atm = ATM.get_instance()
5
6 # Perform Check Balance operation
7 atm.insert_card("1234-5678-9012-3456")
8 atm.enter_pin("1234")
9 atm.select_operation(OperationType.CHECK_BALANCE) # $1000
10
11 # Perform Withdraw Cash operation
12 atm.insert_card("1234-5678-9012-3456")
13 atm.enter_pin("1234")
14 atm.select_operation(OperationType.WITHDRAW_CASH, 570)
15
16 # Perform Deposit Cash operation
17 atm.insert_card("1234-5678-9012-3456")
18 atm.enter_pin("1234")
19 atm.select_operation(OperationType.DEPOSIT_CASH, 200)
20
21 # Perform Check Balance operation
22 atm.insert_card("1234-5678-9012-3456")
23 atm.enter_pin("1234")
24 atm.select_operation(OperationType.CHECK_BALANCE) # $630
25
26 # Perform Withdraw Cash more than balance
27 atm.insert_card("1234-5678-9012-3456")
28 atm.enter_pin("1234")
29 atm.select_operation(OperationType.WITHDRAW_CASH, 700) # Insufficient balance
30
31 # Insert Incorrect PIN
32 atm.insert_card("1234-5678-9012-3456")
33 atm.enter_pin("3425")
34
35if __name__ == "__main__":
36 ATMDemo.main()Which entity is primarily responsible for managing the cash inventory and dispensing cash in an ATM system?
No comments yet. Be the first to comment!