Build a modular, extensible, and maintainable ATM system using core OOP principles and classic design patterns in Java.
π§ Table of Contents
- Introduction
- System Requirements
- Design Goals
- High-Level Class Design
- Design Patterns Used
- Code Structure & Implementation
- Flow Diagram (Optional)
- Sample Output
- Conclusion
- What's Next?
π° Introduction
In system design interviews, the ATM System is a classic LLD question used to evaluate a candidateβs grasp on:
- Object-oriented modeling
- State transitions
- Behavioral design patterns
- Maintainability and testability
This blog presents a clean, extensible ATM implementation in Java with full explanation and code snippets, following SOLID principles and design patterns.
β System Requirements
Functional Requirements:
- Insert ATM Card
- Verify PIN
- Withdraw cash
- Deposit cash
- Check account balance
- Eject card
Non-Functional Requirements:
- Thread-safe and modular
- Easily extendable for new features (e.g., mini statement, fund transfer)
Hereβs a UML class diagram for the ATM System we just designed. It captures:
- Class relationships
- Design pattern usage (State, Strategy, Factory, Singleton)
- Method signatures and key attributes
β UML Diagram β ATM System (ASCII View)
+------------------+
| <<Singleton>>|
| ATM |
+------------------+
| - state: ATMState
| - currentAccount: UserAccount
+------------------+
| +insertCard(Card)
| +enterPin(String)
| +performTransaction(String, double)
| +ejectCard()
| +setState(ATMState)
| +getCurrentAccount(): UserAccount
+------------------+
|
βΌ
+------------------+
| <<interface>> |
| ATMState |
+------------------+
| +insertCard(Card)
| +enterPin(String)
| +performTransaction(String, double)
| +ejectCard()
+------------------+
β²
βββββββββββββΌβββββββββββββ
βΌ βΌ βΌ
+----------------+ +---------------------+ +------------------------+
| IdleState | | CardInsertedState | | AuthenticatedState |
+----------------+ +---------------------+ +------------------------+
| - atm: ATM | | - atm: ATM | | - atm: ATM |
+----------------+ +---------------------+ +------------------------+
| Overrides ATMState methods for each state |
+--------------------------------------------------------------------+
+------------------------+ +--------------------------+
| Card | | UserAccount |
+------------------------+ +--------------------------+
| - cardNumber: String | | - accountNumber: String |
| - linkedAccount: UserAccount| | - pin: String |
+------------------------+ | - balance: double |
| +getLinkedAccount(): UserAccount +--------------------------+
| | +validatePin(String): boolean
| | +credit(double)
| | +debit(double)
+------------------------+ +--------------------------+
+--------------------------+
| <<interface>> |
| Transaction |
+--------------------------+
| +execute(UserAccount, double)
+--------------------------+
β² β²
| |
+------------------+ +--------------------+
| WithdrawTransaction | | DepositTransaction |
+------------------+ +--------------------+
| Overrides execute() | | Overrides execute() |
+------------------+ +--------------------+
+--------------------------+
| TransactionFactory |
+--------------------------+
| +getTransaction(type): Transaction
+--------------------------+
π Legend
-
<<interface>>
: Interface class -
<<Singleton>>
: Singleton instance -
Arrows:
- Solid Arrow (β²): Inheritance/implementation
- Line with arrowhead (β): Association or reference
π§ Key Design Principles Reflected
Principle / Pattern | Where Applied |
---|---|
State Pattern |
ATMState and its implementations |
Strategy Pattern |
Transaction and subtypes |
Factory Pattern | TransactionFactory |
Singleton Pattern |
ATM class |
SRP/OCP | Each state/transaction has a single role and is open for extension |
π― Design Goals
- Encapsulation: Hide internal states and expose meaningful APIs.
- Extensibility: Use interface-driven development to enable easy enhancements.
- Testability: Decouple responsibilities to enable unit testing.
- Pattern-driven architecture: Use GoF design patterns appropriately.
ποΈ High-Level Class Design
Component | Responsibility |
---|---|
ATM |
Main context managing current state and session |
ATMState |
Interface defining ATM behavior at different stages |
UserAccount |
Represents a user's account with balance and PIN |
Card |
Represents a physical ATM card |
Transaction |
Strategy interface for different transactions |
TransactionFactory |
Factory to instantiate transaction strategies |
π§° Design Patterns Used
Pattern | Role |
---|---|
State | Manage ATM workflow states (Idle, Authenticated, etc.) |
Strategy | Encapsulate transaction logic (Withdraw, Deposit) |
Factory | Centralized object creation for transactions |
Singleton | Global ATM instance |
π§± Code Structure & Implementation
π Project Structure
com.atm
βββ atm/ # Core ATM logic
β βββ ATM.java
β βββ ATMState.java
βββ state/ # ATM state implementations
β βββ IdleState.java
β βββ CardInsertedState.java
β βββ AuthenticatedState.java
βββ transaction/ # Transaction strategy layer
β βββ Transaction.java
β βββ WithdrawTransaction.java
β βββ DepositTransaction.java
β βββ TransactionFactory.java
βββ model/ # Domain models
β βββ UserAccount.java
β βββ Card.java
βββ Main.java # Entry point
πΉ ATM.java
β Singleton Context
public class ATM {
private static final ATM instance = new ATM();
private ATMState currentState;
private UserAccount currentAccount;
private ATM() {
this.currentState = new IdleState(this);
}
public static ATM getInstance() {
return instance;
}
public void insertCard(Card card) { currentState.insertCard(card); }
public void enterPin(String pin) { currentState.enterPin(pin); }
public void performTransaction(String type, double amount) { currentState.performTransaction(type, amount); }
public void ejectCard() { currentState.ejectCard(); }
public void setState(ATMState state) { this.currentState = state; }
public void setCurrentAccount(UserAccount account) { this.currentAccount = account; }
public UserAccount getCurrentAccount() { return currentAccount; }
}
πΉ ATMState.java
β State Interface
public interface ATMState {
void insertCard(Card card);
void enterPin(String pin);
void performTransaction(String type, double amount);
void ejectCard();
}
πΉ Sample States
β
IdleState.java
public class IdleState implements ATMState {
private final ATM atm;
public IdleState(ATM atm) { this.atm = atm; }
public void insertCard(Card card) {
System.out.println("Card inserted.");
atm.setCurrentAccount(card.getLinkedAccount());
atm.setState(new CardInsertedState(atm));
}
public void enterPin(String pin) { System.out.println("Insert card first."); }
public void performTransaction(String type, double amount) { System.out.println("Insert card first."); }
public void ejectCard() { System.out.println("Insert card first."); }
}
β
CardInsertedState.java
public class CardInsertedState implements ATMState {
private final ATM atm;
public CardInsertedState(ATM atm) { this.atm = atm; }
public void enterPin(String pin) {
if (atm.getCurrentAccount().validatePin(pin)) {
System.out.println("PIN validated.");
atm.setState(new AuthenticatedState(atm));
} else {
System.out.println("Invalid PIN.");
}
}
public void insertCard(Card card) { System.out.println("Card already inserted."); }
public void performTransaction(String type, double amount) { System.out.println("Enter PIN first."); }
public void ejectCard() {
atm.setCurrentAccount(null);
atm.setState(new IdleState(atm));
System.out.println("Card ejected.");
}
}
πΉ Transaction.java
β Strategy Interface
public interface Transaction {
void execute(UserAccount account, double amount);
}
πΉ WithdrawTransaction.java
public class WithdrawTransaction implements Transaction {
public void execute(UserAccount account, double amount) {
if (account.getBalance() >= amount) {
account.debit(amount);
System.out.println("Withdrawn: βΉ" + amount);
} else {
System.out.println("Insufficient balance.");
}
}
}
πΉ TransactionFactory.java
public class TransactionFactory {
public static Transaction getTransaction(String type) {
switch (type.toLowerCase()) {
case "withdraw": return new WithdrawTransaction();
case "deposit": return new DepositTransaction();
default: return null;
}
}
}
πΉ UserAccount.java
public class UserAccount {
private final String accountNumber;
private final String pin;
private double balance;
public UserAccount(String accountNumber, String pin, double balance) {
this.accountNumber = accountNumber;
this.pin = pin;
this.balance = balance;
}
public boolean validatePin(String inputPin) { return pin.equals(inputPin); }
public double getBalance() { return balance; }
public void debit(double amount) { balance -= amount; }
public void credit(double amount) { balance += amount; }
}
πΉ Main.java
public class Main {
public static void main(String[] args) {
UserAccount account = new UserAccount("123456", "4321", 5000.0);
Card card = new Card("999988887777", account);
ATM atm = ATM.getInstance();
atm.insertCard(card);
atm.enterPin("4321");
atm.performTransaction("withdraw", 1000);
atm.performTransaction("deposit", 500);
atm.ejectCard();
}
}
π Sample Output
Card inserted.
PIN validated.
Withdrawn: βΉ1000.0
Deposited: βΉ500.0
Card ejected.
π§© Flow Diagram (Optional)
[IdleState] -> insertCard() -> [CardInsertedState]
-> enterPin() -> [AuthenticatedState]
-> performTransaction()
-> Transaction Strategy
-> ejectCard() -> [IdleState]
β Conclusion
This design gives you:
- A modular and testable Java-based ATM system
- State transitions managed using the State pattern
- Transaction extensibility using Strategy and Factory
- Domain modeling that maps cleanly to real-world entities
π Whatβs Next?
- Add mini statement feature
- Introduce currency dispenser logic
- Add support for multi-language UI layer
- Integrate with a database-backed UserAccount system
Top comments (0)