DEV Community

DevCorner2
DevCorner2

Posted on

πŸ’³ Designing an ATM System in Java – A Complete Low-Level Design Guide

Build a modular, extensible, and maintainable ATM system using core OOP principles and classic design patterns in Java.


🧭 Table of Contents

  1. Introduction
  2. System Requirements
  3. Design Goals
  4. High-Level Class Design
  5. Design Patterns Used
  6. Code Structure & Implementation
  7. Flow Diagram (Optional)
  8. Sample Output
  9. Conclusion
  10. 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
                   +--------------------------+
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ 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
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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; }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή ATMState.java – State Interface

public interface ATMState {
    void insertCard(Card card);
    void enterPin(String pin);
    void performTransaction(String type, double amount);
    void ejectCard();
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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."); }
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή Transaction.java – Strategy Interface

public interface Transaction {
    void execute(UserAccount account, double amount);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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; }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“ˆ Sample Output

Card inserted.
PIN validated.
Withdrawn: β‚Ή1000.0
Deposited: β‚Ή500.0
Card ejected.
Enter fullscreen mode Exit fullscreen mode

🧩 Flow Diagram (Optional)

[IdleState] -> insertCard() -> [CardInsertedState] 
    -> enterPin() -> [AuthenticatedState] 
        -> performTransaction()
            -> Transaction Strategy
    -> ejectCard() -> [IdleState]
Enter fullscreen mode Exit fullscreen mode

βœ… 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)